engineering-platform 2.2.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.
Potentially problematic release.
This version of engineering-platform might be problematic. Click here for more details.
- engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
- engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
- engineering_platform/__init__.py +1 -0
- engineering_platform/__main__.py +7 -0
- engineering_platform/agent_state.py +530 -0
- engineering_platform/agent_trust.py +174 -0
- engineering_platform/assets/dashboard.css +1317 -0
- engineering_platform/assets/dashboard.js +8534 -0
- engineering_platform/assets/dashboard_locales.mjs +4049 -0
- engineering_platform/assets/dashboard_status_store.mjs +41 -0
- engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-dark.png +0 -0
- engineering_platform/assets/operations-console/icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
- engineering_platform/capability_preflight.py +285 -0
- engineering_platform/capability_review.py +261 -0
- engineering_platform/central_data_transfer.py +195 -0
- engineering_platform/central_database.py +245 -0
- engineering_platform/central_store_migration.py +1672 -0
- engineering_platform/codex_capacity.py +81 -0
- engineering_platform/codex_chat.py +226 -0
- engineering_platform/codex_observability.py +153 -0
- engineering_platform/component_lock.py +40 -0
- engineering_platform/component_logging.py +420 -0
- engineering_platform/console_presentation.py +14 -0
- engineering_platform/console_route_ownership.py +83 -0
- engineering_platform/contracts/__init__.py +38 -0
- engineering_platform/contracts/ep_consumer.py +391 -0
- engineering_platform/contracts/models.py +105 -0
- engineering_platform/contracts/projection.py +401 -0
- engineering_platform/dashboard_browser_validation.py +206 -0
- engineering_platform/dashboard_state.py +630 -0
- engineering_platform/dashboard_supervisor.swift +105 -0
- engineering_platform/dashboard_translation.py +129 -0
- engineering_platform/dependabot_producer.py +349 -0
- engineering_platform/drift_diagnostics.py +144 -0
- engineering_platform/emergency_recovery.py +268 -0
- engineering_platform/engineering_memory.py +139 -0
- engineering_platform/ep_consumer_credentials.py +473 -0
- engineering_platform/evidence_projection.py +213 -0
- engineering_platform/execution_activity.py +218 -0
- engineering_platform/execution_context.py +132 -0
- engineering_platform/execution_errors.py +42 -0
- engineering_platform/execution_evidence.py +24 -0
- engineering_platform/execution_executor.py +730 -0
- engineering_platform/execution_finalization.py +44 -0
- engineering_platform/execution_host.py +3306 -0
- engineering_platform/execution_lease.py +365 -0
- engineering_platform/execution_lifecycle.py +447 -0
- engineering_platform/execution_models.py +43 -0
- engineering_platform/execution_readiness.py +166 -0
- engineering_platform/execution_reporting.py +1607 -0
- engineering_platform/execution_repository.py +253 -0
- engineering_platform/execution_timeout_policy.py +56 -0
- engineering_platform/execution_timing.py +440 -0
- engineering_platform/execution_transaction.py +28 -0
- engineering_platform/external_producer_binding.py +235 -0
- engineering_platform/file_inbox.py +249 -0
- engineering_platform/forensic_attribution.py +338 -0
- engineering_platform/forensic_attribution_v2.py +134 -0
- engineering_platform/forensic_delta.py +299 -0
- engineering_platform/golden_scenario.py +63 -0
- engineering_platform/historical_dashboard_configuration.py +171 -0
- engineering_platform/host_admin.py +199 -0
- engineering_platform/host_preflight.py +231 -0
- engineering_platform/installation_relocation.py +122 -0
- engineering_platform/investigation_ledger.py +89 -0
- engineering_platform/legacy_inbox_migration.py +79 -0
- engineering_platform/lifecycle_worker.py +223 -0
- engineering_platform/live_status.py +267 -0
- engineering_platform/local_api.py +209 -0
- engineering_platform/local_api_keychain.py +51 -0
- engineering_platform/local_repository_binding.py +138 -0
- engineering_platform/managed_autonomy.py +509 -0
- engineering_platform/managed_codex_runtime.py +105 -0
- engineering_platform/parity_context.py +203 -0
- engineering_platform/parity_lifecycle_dispatcher.py +488 -0
- engineering_platform/platform_admin.py +13 -0
- engineering_platform/platform_api.py +428 -0
- engineering_platform/platform_bootstrap.py +385 -0
- engineering_platform/platform_components.py +65 -0
- engineering_platform/platform_version.py +171 -0
- engineering_platform/pr_check_repair.py +276 -0
- engineering_platform/pr_evidence_backfill.py +278 -0
- engineering_platform/producer.py +209 -0
- engineering_platform/project_agent.py +366 -0
- engineering_platform/project_agent_service.py +244 -0
- engineering_platform/project_topology.py +126 -0
- engineering_platform/prompt_history.py +591 -0
- engineering_platform/provider_context.py +136 -0
- engineering_platform/provider_context_benchmark.py +41 -0
- engineering_platform/provider_context_scope.py +90 -0
- engineering_platform/provider_interruption.py +168 -0
- engineering_platform/provider_process_identity.py +80 -0
- engineering_platform/provider_readiness.py +138 -0
- engineering_platform/provider_recovery.py +647 -0
- engineering_platform/provider_usage.py +497 -0
- engineering_platform/providers.py +471 -0
- engineering_platform/qualification.py +220 -0
- engineering_platform/recommendation_handoff.py +238 -0
- engineering_platform/report_analysis.py +193 -0
- engineering_platform/repository_attachment.py +171 -0
- engineering_platform/repository_handoff.py +95 -0
- engineering_platform/resources.py +38 -0
- engineering_platform/reviewer_evidence.py +70 -0
- engineering_platform/schemas/repository-attachment.schema.json +61 -0
- engineering_platform/server.py +3679 -0
- engineering_platform/server_console_services.py +2024 -0
- engineering_platform/server_relay.py +172 -0
- engineering_platform/server_service.py +122 -0
- engineering_platform/status_model.py +135 -0
- engineering_platform/status_reconciliation.py +34 -0
- engineering_platform/storage.py +2440 -0
- engineering_platform/submission_cli.py +77 -0
- engineering_platform/submission_intake.py +45 -0
- engineering_platform/submission_service.py +317 -0
- engineering_platform/telemetry.py +951 -0
- engineering_platform/templates/workspace-config.json +25 -0
- engineering_platform/validation_identity.py +50 -0
- engineering_platform/validation_profile.py +211 -0
- engineering_platform/workspace_preflight.py +263 -0
- engineering_platform/worktree_provenance.py +147 -0
- engineering_platform/worktree_tooling.py +18 -0
- engineering_platform-2.2.0.dist-info/METADATA +18 -0
- engineering_platform-2.2.0.dist-info/RECORD +130 -0
- engineering_platform-2.2.0.dist-info/WHEEL +5 -0
- engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
- engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Read-only projection of an immutable Forge governance handoff snapshot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
FORGE_GOVERNANCE_HANDOFF_VERSION = "1.0"
|
|
12
|
+
_MAX_TEXT = 2_000
|
|
13
|
+
_REPORT = re.compile(r"(?ms)^## Forge Governance Handoff\s*$.*?^### Immutable Handoff Snapshot\s*$\n```json\s*(\{.*?\})\s*```")
|
|
14
|
+
_LEGACY_REPORT = re.compile(r"(?ms)^## Forge Mission Recommendation Handoff\s*$.*?^```json\s*(\{.*?\})\s*```")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ForgeGovernanceHandoffError(ValueError):
|
|
18
|
+
"""Raised only when a supplied Forge handoff cannot meet its transport contract."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _text(value: object) -> str | None:
|
|
22
|
+
if not isinstance(value, str):
|
|
23
|
+
return None
|
|
24
|
+
value = " ".join(value.strip().split())
|
|
25
|
+
return value[:_MAX_TEXT] if value else None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _value(raw: dict[str, Any], *keys: str) -> str | None:
|
|
29
|
+
for key in keys:
|
|
30
|
+
value = _text(raw.get(key))
|
|
31
|
+
if value is not None:
|
|
32
|
+
return value
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _items(value: object) -> tuple[str, ...]:
|
|
37
|
+
if not isinstance(value, list):
|
|
38
|
+
return ()
|
|
39
|
+
return tuple(item for entry in value if (item := _text(entry)))[:50]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _rank(value: object) -> int | None:
|
|
43
|
+
return value if isinstance(value, int) and not isinstance(value, bool) and value > 0 else None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def validate_forge_governance_handoff(payload: object) -> dict[str, object]:
|
|
47
|
+
"""Validate transport shape without deriving any Forge governance meaning.
|
|
48
|
+
|
|
49
|
+
Unknown future fields are retained verbatim. Known fields are checked only
|
|
50
|
+
when supplied, so legacy and incrementally richer Forge producers remain
|
|
51
|
+
compatible while malformed supplied handoffs fail closed at ingress.
|
|
52
|
+
"""
|
|
53
|
+
if not isinstance(payload, dict):
|
|
54
|
+
raise ForgeGovernanceHandoffError("Forge Governance Handoff payload must be an object.")
|
|
55
|
+
if payload.get("version") != FORGE_GOVERNANCE_HANDOFF_VERSION:
|
|
56
|
+
raise ForgeGovernanceHandoffError("Forge Governance Handoff version is unsupported.")
|
|
57
|
+
for name in ("recommendation_set", "selected_recommendation", "decision_evidence", "business_workspace", "governance", "mission_runtime"):
|
|
58
|
+
if name in payload and payload[name] is not None and not isinstance(payload[name], dict):
|
|
59
|
+
raise ForgeGovernanceHandoffError(f"Forge Governance Handoff {name} must be an object when supplied.")
|
|
60
|
+
alternatives = payload.get("alternatives", payload.get("alternative_recommendations"))
|
|
61
|
+
if alternatives is not None:
|
|
62
|
+
if not isinstance(alternatives, list) or not all(isinstance(item, dict) for item in alternatives):
|
|
63
|
+
raise ForgeGovernanceHandoffError("Forge Governance Handoff alternatives must be an array of objects when supplied.")
|
|
64
|
+
selected = payload.get("selected_recommendation")
|
|
65
|
+
if selected is not None and not isinstance(selected, dict):
|
|
66
|
+
raise ForgeGovernanceHandoffError("Forge Governance Handoff selected_recommendation must be an object when supplied.")
|
|
67
|
+
return payload
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class Recommendation:
|
|
72
|
+
recommendation_id: str | None
|
|
73
|
+
rank: int | None
|
|
74
|
+
title: str | None
|
|
75
|
+
mission_origin: str | None
|
|
76
|
+
lifecycle_status: str | None
|
|
77
|
+
business_summary: str | None
|
|
78
|
+
engineering_summary: str | None
|
|
79
|
+
business_value: str | None
|
|
80
|
+
architecture_value: str | None
|
|
81
|
+
engineering_value: str | None
|
|
82
|
+
risk_if_deferred: str | None
|
|
83
|
+
dependencies: tuple[str, ...]
|
|
84
|
+
constraints: tuple[str, ...]
|
|
85
|
+
confidence: str | None
|
|
86
|
+
evidence_references: tuple[str, ...]
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def from_mapping(cls, raw: object) -> "Recommendation":
|
|
90
|
+
mapping = raw if isinstance(raw, dict) else {}
|
|
91
|
+
return cls(
|
|
92
|
+
recommendation_id=_value(mapping, "recommendation_id", "id"), rank=_rank(mapping.get("rank")),
|
|
93
|
+
title=_value(mapping, "title", "recommendation_title"), mission_origin=_value(mapping, "mission_origin"),
|
|
94
|
+
lifecycle_status=_value(mapping, "lifecycle_status", "status", "recommendation_status"),
|
|
95
|
+
business_summary=_value(mapping, "business_summary", "summary"), engineering_summary=_value(mapping, "engineering_summary"),
|
|
96
|
+
business_value=_value(mapping, "business_value"), architecture_value=_value(mapping, "architecture_value", "architectural_value"),
|
|
97
|
+
engineering_value=_value(mapping, "engineering_value"), risk_if_deferred=_value(mapping, "risk_if_deferred"),
|
|
98
|
+
dependencies=_items(mapping.get("dependencies")), constraints=_items(mapping.get("constraints")),
|
|
99
|
+
confidence=_value(mapping, "confidence"), evidence_references=_items(mapping.get("evidence_references")),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class ForgeGovernanceHandoff:
|
|
105
|
+
snapshot: dict[str, object]
|
|
106
|
+
recommendation_set_id: str | None
|
|
107
|
+
recommendation_set_timestamp: str | None
|
|
108
|
+
recommendation_count: int | None
|
|
109
|
+
selected: Recommendation
|
|
110
|
+
alternatives: tuple[Recommendation, ...]
|
|
111
|
+
decision_evidence: dict[str, object] | None
|
|
112
|
+
business_workspace: dict[str, object] | None
|
|
113
|
+
governance: dict[str, object] | None
|
|
114
|
+
mission_runtime: dict[str, object] | None
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_snapshot(cls, snapshot: dict[str, object]) -> "ForgeGovernanceHandoff":
|
|
118
|
+
data = validate_forge_governance_handoff(snapshot)
|
|
119
|
+
recommendation_set = data.get("recommendation_set") if isinstance(data.get("recommendation_set"), dict) else {}
|
|
120
|
+
selected_raw = data.get("selected_recommendation") if isinstance(data.get("selected_recommendation"), dict) else {}
|
|
121
|
+
alternatives_raw = data.get("alternatives", data.get("alternative_recommendations", []))
|
|
122
|
+
alternatives = tuple(Recommendation.from_mapping(item) for item in alternatives_raw if isinstance(item, dict)) if isinstance(alternatives_raw, list) else ()
|
|
123
|
+
count = recommendation_set.get("count", data.get("recommendation_count"))
|
|
124
|
+
return cls(
|
|
125
|
+
snapshot=data, recommendation_set_id=_value(recommendation_set, "id", "recommendation_set_id") or _value(data, "recommendation_set_id"),
|
|
126
|
+
recommendation_set_timestamp=_value(recommendation_set, "timestamp", "created_at") or _value(data, "recommendation_set_timestamp"),
|
|
127
|
+
recommendation_count=count if isinstance(count, int) and not isinstance(count, bool) and count >= 0 else None,
|
|
128
|
+
selected=Recommendation.from_mapping(selected_raw), alternatives=alternatives,
|
|
129
|
+
decision_evidence=data.get("decision_evidence") if isinstance(data.get("decision_evidence"), dict) else None,
|
|
130
|
+
business_workspace=data.get("business_workspace") if isinstance(data.get("business_workspace"), dict) else None,
|
|
131
|
+
governance=data.get("governance") if isinstance(data.get("governance"), dict) else None,
|
|
132
|
+
mission_runtime=data.get("mission_runtime") if isinstance(data.get("mission_runtime"), dict) else None,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def completeness(self) -> str:
|
|
137
|
+
required = (self.selected.recommendation_id, self.selected.title, self.selected.rank, self.selected.lifecycle_status, self.selected.confidence, self.decision_evidence_id, self.alternatives, self.business_approval_state)
|
|
138
|
+
return "COMPLETE" if all(value is not None and value != () for value in required) else "INCOMPLETE"
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def decision_evidence_id(self) -> str | None:
|
|
142
|
+
return _value(self.decision_evidence or {}, "id", "decision_evidence_id")
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def business_approval_state(self) -> str | None:
|
|
146
|
+
return _value(self.governance or {}, "business_approval_state")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _display(value: object) -> str:
|
|
150
|
+
if isinstance(value, tuple):
|
|
151
|
+
return ", ".join(value) or "NOT SUPPLIED BY PRODUCER"
|
|
152
|
+
return str(value) if value is not None else "NOT SUPPLIED BY PRODUCER"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _provenance(source: str) -> str:
|
|
156
|
+
return f"[Source: {source}]"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def handoff_from_report(report: str) -> dict[str, object] | None:
|
|
160
|
+
"""Read the already-rendered immutable snapshot for legacy report detail views."""
|
|
161
|
+
match = _REPORT.search(report) or _LEGACY_REPORT.search(report)
|
|
162
|
+
if match is None:
|
|
163
|
+
return None
|
|
164
|
+
try:
|
|
165
|
+
payload = json.loads(match.group(1))
|
|
166
|
+
except json.JSONDecodeError:
|
|
167
|
+
return None
|
|
168
|
+
return payload if isinstance(payload, dict) else None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def report_lines(handoff: ForgeGovernanceHandoff | None, execution_status: str) -> tuple[str, ...]:
|
|
172
|
+
"""Render only values explicitly supplied in the immutable snapshot."""
|
|
173
|
+
if handoff is None:
|
|
174
|
+
return (
|
|
175
|
+
"## Forge Governance Handoff", "- Forge Governance Handoff: `NOT SUPPLIED BY PRODUCER`",
|
|
176
|
+
"- Governance Status: `FORGE GOVERNANCE HANDOFF NOT SUPPLIED`", "- Decision Evidence: `NOT SUPPLIED BY PRODUCER`",
|
|
177
|
+
"- Governance Handoff Completeness: `INCOMPLETE`", "- Business Review Readiness: `NOT SUPPLIED BY PRODUCER`", "",
|
|
178
|
+
)
|
|
179
|
+
selected = handoff.selected
|
|
180
|
+
evidence = handoff.decision_evidence or {}
|
|
181
|
+
workspace = handoff.business_workspace or {}
|
|
182
|
+
governance = handoff.governance or {}
|
|
183
|
+
runtime = handoff.mission_runtime or {}
|
|
184
|
+
governance_status = _value(governance, "status", "governance_status") or "FORGE GOVERNANCE HANDOFF SUPPLIED"
|
|
185
|
+
readiness = _value(governance, "business_review_readiness", "readiness")
|
|
186
|
+
lines = [
|
|
187
|
+
"## Forge Governance Handoff",
|
|
188
|
+
"Forge owns governance truth. Engineering Platform persists and projects this supplied snapshot immutably; completeness is factual reporting, not governance authority.",
|
|
189
|
+
"- Forge Governance Handoff: `SUPPLIED BY PRODUCER; PERSISTED IMMUTABLY`",
|
|
190
|
+
f"- Governance Status: `{governance_status}` {_provenance('Forge Governance Handoff payload')}",
|
|
191
|
+
f"- Governance Handoff Completeness: `{handoff.completeness}` {_provenance('Forge Governance Handoff payload')}",
|
|
192
|
+
f"- Business Review Readiness: `{_display(readiness)}` {_provenance('Forge Governance Handoff payload')}",
|
|
193
|
+
f"- Recommendation Set ID: `{_display(handoff.recommendation_set_id)}` {_provenance('Forge Governance Handoff payload')}",
|
|
194
|
+
f"- Recommendation Set Timestamp: {_display(handoff.recommendation_set_timestamp)} {_provenance('Forge Governance Handoff payload')}",
|
|
195
|
+
f"- Recommendation Count: {_display(handoff.recommendation_count)} {_provenance('Forge Governance Handoff payload')}",
|
|
196
|
+
f"- Selected Recommendation ID: `{_display(selected.recommendation_id)}` {_provenance('Forge Governance Handoff payload')}",
|
|
197
|
+
f"- Selected Recommendation Rank: {_display(selected.rank)} {_provenance('Forge Governance Handoff payload')}",
|
|
198
|
+
f"- Selected Recommendation Title: {_display(selected.title)} {_provenance('Forge Governance Handoff payload')}",
|
|
199
|
+
f"- Selected Mission Origin: {_display(selected.mission_origin)} {_provenance('Forge Governance Handoff payload')}",
|
|
200
|
+
f"- Selected Lifecycle Status: {_display(selected.lifecycle_status)} {_provenance('Forge Governance Handoff payload')}",
|
|
201
|
+
f"- Selected Business Summary: {_display(selected.business_summary)} {_provenance('Forge Governance Handoff payload')}",
|
|
202
|
+
f"- Selected Engineering Summary: {_display(selected.engineering_summary)} {_provenance('Forge Governance Handoff payload')}",
|
|
203
|
+
f"- Selected Business Value: {_display(selected.business_value)} {_provenance('Forge Governance Handoff payload')}",
|
|
204
|
+
f"- Selected Architecture Value: {_display(selected.architecture_value)} {_provenance('Forge Governance Handoff payload')}",
|
|
205
|
+
f"- Selected Engineering Value: {_display(selected.engineering_value)} {_provenance('Forge Governance Handoff payload')}",
|
|
206
|
+
f"- Selected Risk If Deferred: {_display(selected.risk_if_deferred)} {_provenance('Forge Governance Handoff payload')}",
|
|
207
|
+
f"- Selected Dependencies: {_display(selected.dependencies)} {_provenance('Forge Governance Handoff payload')}",
|
|
208
|
+
f"- Selected Constraints: {_display(selected.constraints)} {_provenance('Forge Governance Handoff payload')}",
|
|
209
|
+
f"- Selected Confidence: {_display(selected.confidence)} {_provenance('Forge Governance Handoff payload')}",
|
|
210
|
+
f"- Selected Evidence References: {_display(selected.evidence_references)} {_provenance('Forge Governance Handoff payload')}",
|
|
211
|
+
f"- Decision Evidence ID: `{_display(_value(evidence, 'id', 'decision_evidence_id'))}` {_provenance('Forge Governance Handoff payload')}",
|
|
212
|
+
f"- Decision Evidence Type: {_display(_value(evidence, 'type', 'decision_evidence_type'))} {_provenance('Forge Governance Handoff payload')}",
|
|
213
|
+
f"- Decision Evidence Timestamp: {_display(_value(evidence, 'timestamp', 'created_at'))} {_provenance('Forge Governance Handoff payload')}",
|
|
214
|
+
f"- Decision Evidence Confidence: {_display(_value(evidence, 'confidence'))} {_provenance('Forge Governance Handoff payload')}",
|
|
215
|
+
f"- Decision Evidence References: {_display(_items(evidence.get('references')))} {_provenance('Forge Governance Handoff payload')}",
|
|
216
|
+
f"- Business Workspace Availability: {_display(_value(workspace, 'availability', 'state'))} {_provenance('Forge Governance Handoff payload')}",
|
|
217
|
+
f"- Business Workspace Reference: {_display(_value(workspace, 'reference', 'id'))} {_provenance('Forge Governance Handoff payload')}",
|
|
218
|
+
f"- Business Workspace Projected Selected Recommendation ID: {_display(_value(workspace, 'projected_selected_recommendation_id'))} {_provenance('Forge Governance Handoff payload')}",
|
|
219
|
+
f"- Business Workspace Projection Match State: {_display(_value(workspace, 'projection_match_state'))} {_provenance('Forge Governance Handoff payload')}",
|
|
220
|
+
f"- Business Approval State: {_display(_value(governance, 'business_approval_state'))} {_provenance('Forge Governance Handoff payload')}",
|
|
221
|
+
f"- Architecture Approval State: {_display(_value(governance, 'architecture_approval_state'))} {_provenance('Forge Governance Handoff payload')}",
|
|
222
|
+
f"- Mission Candidate ID: `{_display(_value(runtime, 'mission_candidate_id'))}` {_provenance('Forge Governance Handoff payload')}",
|
|
223
|
+
f"- Mission ID: `{_display(_value(runtime, 'mission_id'))}` {_provenance('Forge Governance Handoff payload')}",
|
|
224
|
+
f"- Mission Runtime State: {_display(_value(runtime, 'mission_runtime_state'))} {_provenance('Forge Governance Handoff payload')}",
|
|
225
|
+
f"- Scheduler State: {_display(_value(runtime, 'scheduler_state'))} {_provenance('Forge Governance Handoff payload')}",
|
|
226
|
+
"### Alternatives",
|
|
227
|
+
]
|
|
228
|
+
if not handoff.alternatives:
|
|
229
|
+
lines.append("- Alternatives: `NOT SUPPLIED BY PRODUCER`")
|
|
230
|
+
for item in handoff.alternatives:
|
|
231
|
+
lines.extend((
|
|
232
|
+
f"- Recommendation ID: `{_display(item.recommendation_id)}`; Rank: {_display(item.rank)}; Title: {_display(item.title)}",
|
|
233
|
+
f" - Mission Origin: {_display(item.mission_origin)}; Lifecycle Status: {_display(item.lifecycle_status)}",
|
|
234
|
+
f" - Business Value: {_display(item.business_value)}; Architecture Value: {_display(item.architecture_value)}; Engineering Value: {_display(item.engineering_value)}",
|
|
235
|
+
f" - Risk If Deferred: {_display(item.risk_if_deferred)}; Dependencies: {_display(item.dependencies)}; Confidence: {_display(item.confidence)} {_provenance('Forge Governance Handoff payload')}",
|
|
236
|
+
))
|
|
237
|
+
lines.extend(("### Immutable Handoff Snapshot", "```json", json.dumps(handoff.snapshot, sort_keys=True), "```", ""))
|
|
238
|
+
return tuple(lines)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Read-only, redacted Codex analysis of one terminal Engineering Report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import tempfile
|
|
9
|
+
|
|
10
|
+
from .agent_state import redact_diagnostic
|
|
11
|
+
from .providers import CodexCliProvider
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MAX_ANALYSIS_LENGTH = 8_000
|
|
15
|
+
MAX_REPORT_CONTEXT_LENGTH = 300_000
|
|
16
|
+
_RUN_ID = __import__("re").compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AnalysisProcessingError(ValueError):
|
|
20
|
+
"""A safe, user-presentable reason for rejecting advisory output."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_PROCESSING_REASONS = {
|
|
24
|
+
"processed": "De Codex-analyse is volgens het verplichte structuurcontract verwerkt.",
|
|
25
|
+
"report_unavailable": "Het Engineeringrapport was niet beschikbaar voor deze analyse.",
|
|
26
|
+
"provider_failed": "De EP-beheerde Codex CLI kon de analyseopdracht niet afronden.",
|
|
27
|
+
"provider_unavailable": "De EP-beheerde Codex CLI was niet beschikbaar voor de analyseopdracht.",
|
|
28
|
+
"invalid_structured_response": "Codex gaf geen geldig gestructureerd analyseantwoord terug.",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _final_message(output: str) -> str:
|
|
33
|
+
"""Extract the terminal agent message from Codex JSONL output."""
|
|
34
|
+
for line in reversed(output.splitlines()):
|
|
35
|
+
try:
|
|
36
|
+
event = json.loads(line)
|
|
37
|
+
except json.JSONDecodeError:
|
|
38
|
+
continue
|
|
39
|
+
item = event.get("item") if isinstance(event, dict) else None
|
|
40
|
+
if (
|
|
41
|
+
event.get("type") == "item.completed"
|
|
42
|
+
and isinstance(item, dict)
|
|
43
|
+
and item.get("type") == "agent_message"
|
|
44
|
+
and isinstance(item.get("text"), str)
|
|
45
|
+
):
|
|
46
|
+
return item["text"]
|
|
47
|
+
return output.strip().splitlines()[-1] if output.strip() else ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _schema() -> dict[str, object]:
|
|
51
|
+
return {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": False,
|
|
54
|
+
"properties": {
|
|
55
|
+
"summary": {"type": "string"},
|
|
56
|
+
"findings": {"type": "array", "items": {"type": "string"}},
|
|
57
|
+
"issues": {"type": "array", "items": {"type": "string"}},
|
|
58
|
+
"risks": {"type": "array", "items": {"type": "string"}},
|
|
59
|
+
"next_steps": {"type": "array", "items": {"type": "string"}},
|
|
60
|
+
"product_architect_advice": {"type": "string"},
|
|
61
|
+
},
|
|
62
|
+
"required": (
|
|
63
|
+
"summary",
|
|
64
|
+
"findings",
|
|
65
|
+
"issues",
|
|
66
|
+
"risks",
|
|
67
|
+
"next_steps",
|
|
68
|
+
"product_architect_advice",
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _markdown(payload: dict[str, object], *, processing_status: str = "processed") -> str:
|
|
74
|
+
def items(value: object) -> str:
|
|
75
|
+
values = value if isinstance(value, list) else []
|
|
76
|
+
lines = [f"- {redact_diagnostic(str(item), limit=1_000)}" for item in values if isinstance(item, str)]
|
|
77
|
+
return "\n".join(lines) if lines else "- Geen vastgesteld."
|
|
78
|
+
|
|
79
|
+
return "\n".join(
|
|
80
|
+
(
|
|
81
|
+
"# Codex-analyse van Engineeringrapport",
|
|
82
|
+
"",
|
|
83
|
+
"Deze analyse is adviserend. Het terminale checkpoint, repositorybewijs, commits en validatie blijven de bron van waarheid.",
|
|
84
|
+
"",
|
|
85
|
+
"## Analyseverwerking",
|
|
86
|
+
f"- Status: `{processing_status}`",
|
|
87
|
+
f"- Reden: {_PROCESSING_REASONS[processing_status]}",
|
|
88
|
+
"",
|
|
89
|
+
"## Samenvatting",
|
|
90
|
+
redact_diagnostic(str(payload.get("summary", "Analyse niet beschikbaar.")), limit=1_500),
|
|
91
|
+
"",
|
|
92
|
+
"## Bevindingen",
|
|
93
|
+
items(payload.get("findings")),
|
|
94
|
+
"",
|
|
95
|
+
"## Issues",
|
|
96
|
+
items(payload.get("issues")),
|
|
97
|
+
"",
|
|
98
|
+
"## Risico's",
|
|
99
|
+
items(payload.get("risks")),
|
|
100
|
+
"",
|
|
101
|
+
"## Volgende stappen",
|
|
102
|
+
items(payload.get("next_steps")),
|
|
103
|
+
"",
|
|
104
|
+
"## Advies aan Product Architect",
|
|
105
|
+
redact_diagnostic(str(payload.get("product_architect_advice", "Geen aanvullend advies beschikbaar.")), limit=1_500),
|
|
106
|
+
"",
|
|
107
|
+
)
|
|
108
|
+
)[:MAX_ANALYSIS_LENGTH]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _write(root: Path, run_id: str, value: str) -> Path:
|
|
112
|
+
directory = root / ".engineering" / "report-analysis"
|
|
113
|
+
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
114
|
+
destination = directory / f"{run_id}.md"
|
|
115
|
+
descriptor, temporary = tempfile.mkstemp(prefix=f".{run_id}.", suffix=".tmp", dir=directory)
|
|
116
|
+
try:
|
|
117
|
+
os.fchmod(descriptor, 0o600)
|
|
118
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
119
|
+
handle.write(value)
|
|
120
|
+
handle.flush()
|
|
121
|
+
os.fsync(handle.fileno())
|
|
122
|
+
os.replace(temporary, destination)
|
|
123
|
+
finally:
|
|
124
|
+
Path(temporary).unlink(missing_ok=True)
|
|
125
|
+
return destination
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _fallback(root: Path, run_id: str, status: str, summary: str) -> Path:
|
|
129
|
+
"""Write a controlled processing reason without persisting provider output."""
|
|
130
|
+
return _write(root, run_id, _markdown({"summary": summary}, processing_status=status))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _payload(output: str) -> dict[str, object]:
|
|
134
|
+
"""Accept only an object containing each required structured field."""
|
|
135
|
+
try:
|
|
136
|
+
raw = json.loads(_final_message(output))
|
|
137
|
+
except json.JSONDecodeError as error:
|
|
138
|
+
raise AnalysisProcessingError("invalid structured response") from error
|
|
139
|
+
if not isinstance(raw, dict) or set(_schema()["required"]) - set(raw):
|
|
140
|
+
raise AnalysisProcessingError("invalid structured response")
|
|
141
|
+
return raw
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _bounded_report_context(report_text: str) -> str:
|
|
145
|
+
"""Keep advisory input below the provider limit without altering report evidence."""
|
|
146
|
+
if len(report_text) <= MAX_REPORT_CONTEXT_LENGTH:
|
|
147
|
+
return report_text
|
|
148
|
+
first = MAX_REPORT_CONTEXT_LENGTH // 2
|
|
149
|
+
last = MAX_REPORT_CONTEXT_LENGTH - first
|
|
150
|
+
omitted = len(report_text) - first - last
|
|
151
|
+
return (
|
|
152
|
+
report_text[:first]
|
|
153
|
+
+ f"\n\n[Analysecontext ingekort: {omitted} tekens uit het midden zijn niet aan Codex aangeboden. "
|
|
154
|
+
"Het volledige Engineeringrapport blijft de bron van waarheid.]\n\n"
|
|
155
|
+
+ report_text[-last:]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def analyze(root: Path, run_id: str, report: Path) -> Path:
|
|
160
|
+
"""Persist a bounded advisory analysis; failure never changes transaction outcome."""
|
|
161
|
+
if not _RUN_ID.fullmatch(run_id):
|
|
162
|
+
raise ValueError("run_id is invalid")
|
|
163
|
+
try:
|
|
164
|
+
report_text = report.read_text(encoding="utf-8")
|
|
165
|
+
except OSError:
|
|
166
|
+
return _fallback(root, run_id, "report_unavailable", "Engineeringrapport was niet beschikbaar voor analyse.")
|
|
167
|
+
prompt = """Analyseer uitsluitend het onderstaande Engineeringrapport. Voer geen commando's uit, wijzig geen bestanden, doe geen netwerkverzoeken en doe geen aannames buiten het rapport. Het resultaat is adviserend: repositorybewijs, commits, validatie en het terminale checkpoint zijn altijd leidend. Geef compacte, feitelijke Nederlandse tekst voor samenvatting, bevindingen, issues, risico's, volgende stappen en advies aan de Product Architect. Herhaal geen geheimen, promptinhoud of ruwe loguitvoer.\n\nENGINEERINGRAPPORT:\n""" + _bounded_report_context(report_text)
|
|
168
|
+
schema_path: Path | None = None
|
|
169
|
+
try:
|
|
170
|
+
state_directory = root / ".engineering"
|
|
171
|
+
state_directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
172
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".json", dir=state_directory, delete=False) as handle:
|
|
173
|
+
json.dump(_schema(), handle)
|
|
174
|
+
schema_path = Path(handle.name)
|
|
175
|
+
completed = CodexCliProvider().invoke(
|
|
176
|
+
root,
|
|
177
|
+
(
|
|
178
|
+
"codex", "exec", "--sandbox", "read-only", "-C", str(root), "--json",
|
|
179
|
+
"--output-schema", str(schema_path), "-",
|
|
180
|
+
),
|
|
181
|
+
input_text=prompt,
|
|
182
|
+
)
|
|
183
|
+
if completed.returncode:
|
|
184
|
+
return _fallback(root, run_id, "provider_failed", "Codex-analyse kon niet worden uitgevoerd. De Engineering-uitkomst blijft ongewijzigd.")
|
|
185
|
+
raw = _payload(completed.stdout)
|
|
186
|
+
return _write(root, run_id, _markdown(raw))
|
|
187
|
+
except OSError:
|
|
188
|
+
return _fallback(root, run_id, "provider_unavailable", "Codex-analyse kon niet worden uitgevoerd. De Engineering-uitkomst blijft ongewijzigd.")
|
|
189
|
+
except AnalysisProcessingError:
|
|
190
|
+
return _fallback(root, run_id, "invalid_structured_response", "Codex-analyse kon niet veilig worden verwerkt. De Engineering-uitkomst blijft ongewijzigd.")
|
|
191
|
+
finally:
|
|
192
|
+
if schema_path is not None:
|
|
193
|
+
schema_path.unlink(missing_ok=True)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Read-only repository attachment contract for future EP Project Agents.
|
|
2
|
+
|
|
3
|
+
This module validates declarations only. It never executes a validation
|
|
4
|
+
entrypoint, probes a host, writes local state, or registers with a server.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any, Mapping
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CONFIG_RELATIVE_PATH = Path(".engineering-platform/repository.json")
|
|
16
|
+
SCHEMA_VERSION = "1.0"
|
|
17
|
+
SCHEMA_RESOURCE = "schemas/repository-attachment.schema.json"
|
|
18
|
+
_IDENTIFIER = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
19
|
+
_METADATA_KEY = re.compile(r"^[a-z][a-z0-9._-]*$")
|
|
20
|
+
_ROLES = frozenset({"authority", "child"})
|
|
21
|
+
_VALIDATION_KINDS = frozenset({"command", "script", "workflow", "none"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RepositoryAttachmentError(ValueError):
|
|
25
|
+
"""Raised when a repository attachment is absent or structurally unsafe."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ValidationBoundary:
|
|
30
|
+
kind: str
|
|
31
|
+
entrypoint: str | None
|
|
32
|
+
description: str | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class RepositoryAttachment:
|
|
37
|
+
project_id: str
|
|
38
|
+
authority_repository_id: str
|
|
39
|
+
repository_id: str
|
|
40
|
+
repository_role: str
|
|
41
|
+
validation: ValidationBoundary
|
|
42
|
+
host_requirements: Mapping[str, str]
|
|
43
|
+
tool_requirements: Mapping[str, str]
|
|
44
|
+
integrations: Mapping[str, Mapping[str, Any]]
|
|
45
|
+
|
|
46
|
+
def agent_read_surface(self) -> dict[str, object]:
|
|
47
|
+
"""Return the deliberately small, non-secret future Agent read surface."""
|
|
48
|
+
return {
|
|
49
|
+
"schema_version": SCHEMA_VERSION,
|
|
50
|
+
"project": {
|
|
51
|
+
"id": self.project_id,
|
|
52
|
+
"authority_repository_id": self.authority_repository_id,
|
|
53
|
+
},
|
|
54
|
+
"repository": {"id": self.repository_id, "role": self.repository_role},
|
|
55
|
+
"validation": {
|
|
56
|
+
"kind": self.validation.kind,
|
|
57
|
+
"entrypoint": self.validation.entrypoint,
|
|
58
|
+
"description": self.validation.description,
|
|
59
|
+
},
|
|
60
|
+
"requirements": {"host": dict(self.host_requirements), "tools": dict(self.tool_requirements)},
|
|
61
|
+
"integrations": {name: dict(metadata) for name, metadata in self.integrations.items()},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def config_path(repository_root: Path) -> Path:
|
|
66
|
+
"""Return the one canonical repository-local attachment location."""
|
|
67
|
+
return repository_root / CONFIG_RELATIVE_PATH
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_repository_attachment(repository_root: Path) -> RepositoryAttachment:
|
|
71
|
+
"""Load and validate one repository declaration without discovering peers."""
|
|
72
|
+
path = config_path(repository_root)
|
|
73
|
+
try:
|
|
74
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
75
|
+
except FileNotFoundError as error:
|
|
76
|
+
raise RepositoryAttachmentError(f"repository attachment is missing: {CONFIG_RELATIVE_PATH}") from error
|
|
77
|
+
except json.JSONDecodeError as error:
|
|
78
|
+
raise RepositoryAttachmentError("repository attachment is not valid JSON") from error
|
|
79
|
+
return parse_repository_attachment(payload)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_repository_attachment(payload: object) -> RepositoryAttachment:
|
|
83
|
+
"""Validate JSON-compatible attachment data against schema version 1.0."""
|
|
84
|
+
root = _object(payload, "attachment")
|
|
85
|
+
_only(root, {"schema_version", "project", "repository", "validation", "requirements", "integrations"}, "attachment")
|
|
86
|
+
if root.get("schema_version") != SCHEMA_VERSION:
|
|
87
|
+
raise RepositoryAttachmentError("unsupported repository attachment schema_version")
|
|
88
|
+
|
|
89
|
+
project = _object(_required(root, "project", "attachment"), "project")
|
|
90
|
+
_only(project, {"id", "authority_repository_id"}, "project")
|
|
91
|
+
project_id = _identifier(_required(project, "id", "project"), "project.id")
|
|
92
|
+
authority_repository_id = _identifier(_required(project, "authority_repository_id", "project"), "project.authority_repository_id")
|
|
93
|
+
|
|
94
|
+
repository = _object(_required(root, "repository", "attachment"), "repository")
|
|
95
|
+
_only(repository, {"id", "role"}, "repository")
|
|
96
|
+
repository_id = _identifier(_required(repository, "id", "repository"), "repository.id")
|
|
97
|
+
role = _required(repository, "role", "repository")
|
|
98
|
+
if role not in _ROLES:
|
|
99
|
+
raise RepositoryAttachmentError("repository.role must be authority or child")
|
|
100
|
+
if (role == "authority") != (repository_id == authority_repository_id):
|
|
101
|
+
raise RepositoryAttachmentError("repository.role and project.authority_repository_id disagree")
|
|
102
|
+
|
|
103
|
+
validation_data = _object(_required(root, "validation", "attachment"), "validation")
|
|
104
|
+
_only(validation_data, {"kind", "entrypoint", "description"}, "validation")
|
|
105
|
+
kind = _required(validation_data, "kind", "validation")
|
|
106
|
+
if kind not in _VALIDATION_KINDS:
|
|
107
|
+
raise RepositoryAttachmentError("validation.kind is unsupported")
|
|
108
|
+
entrypoint = validation_data.get("entrypoint")
|
|
109
|
+
if kind == "none" and entrypoint is not None:
|
|
110
|
+
raise RepositoryAttachmentError("validation.entrypoint is forbidden when validation.kind is none")
|
|
111
|
+
if kind != "none":
|
|
112
|
+
entrypoint = _bounded_text(entrypoint, "validation.entrypoint")
|
|
113
|
+
description = validation_data.get("description")
|
|
114
|
+
if description is not None:
|
|
115
|
+
description = _bounded_text(description, "validation.description")
|
|
116
|
+
|
|
117
|
+
requirements = _object(root.get("requirements", {}), "requirements")
|
|
118
|
+
_only(requirements, {"host", "tools"}, "requirements")
|
|
119
|
+
host = _string_map(requirements.get("host", {}), "requirements.host")
|
|
120
|
+
tools = _string_map(requirements.get("tools", {}), "requirements.tools")
|
|
121
|
+
integrations = _integration_map(root.get("integrations", {}))
|
|
122
|
+
return RepositoryAttachment(project_id, authority_repository_id, repository_id, role, ValidationBoundary(kind, entrypoint, description), host, tools, integrations)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _required(mapping: Mapping[str, Any], key: str, context: str) -> Any:
|
|
126
|
+
if key not in mapping:
|
|
127
|
+
raise RepositoryAttachmentError(f"{context}.{key} is required")
|
|
128
|
+
return mapping[key]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _object(value: object, context: str) -> Mapping[str, Any]:
|
|
132
|
+
if not isinstance(value, dict):
|
|
133
|
+
raise RepositoryAttachmentError(f"{context} must be an object")
|
|
134
|
+
return value
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _only(mapping: Mapping[str, Any], allowed: set[str], context: str) -> None:
|
|
138
|
+
unexpected = set(mapping) - allowed
|
|
139
|
+
if unexpected:
|
|
140
|
+
raise RepositoryAttachmentError(f"{context} has unsupported fields: {', '.join(sorted(unexpected))}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _identifier(value: object, context: str) -> str:
|
|
144
|
+
if not isinstance(value, str) or not 3 <= len(value) <= 128 or not _IDENTIFIER.fullmatch(value):
|
|
145
|
+
raise RepositoryAttachmentError(f"{context} must be a lowercase stable identifier")
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _bounded_text(value: object, context: str) -> str:
|
|
150
|
+
if not isinstance(value, str) or not value or len(value) > 512 or "\x00" in value or "\n" in value or "\r" in value:
|
|
151
|
+
raise RepositoryAttachmentError(f"{context} must be one non-empty line of at most 512 characters")
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _string_map(value: object, context: str) -> Mapping[str, str]:
|
|
156
|
+
mapping = _object(value, context)
|
|
157
|
+
return {_metadata_key(key, f"{context} key"): _bounded_text(item, f"{context}.{key}") for key, item in mapping.items()}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _integration_map(value: object) -> Mapping[str, Mapping[str, Any]]:
|
|
161
|
+
mapping = _object(value, "integrations")
|
|
162
|
+
result: dict[str, Mapping[str, Any]] = {}
|
|
163
|
+
for name, metadata in mapping.items():
|
|
164
|
+
result[_metadata_key(name, "integrations key")] = _object(metadata, f"integrations.{name}")
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _metadata_key(value: object, context: str) -> str:
|
|
169
|
+
if not isinstance(value, str) or not 1 <= len(value) <= 128 or not _METADATA_KEY.fullmatch(value):
|
|
170
|
+
raise RepositoryAttachmentError(f"{context} must be a lowercase metadata key")
|
|
171
|
+
return value
|