xrefkit 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. xrefkit/__init__.py +5 -0
  2. xrefkit/__main__.py +5 -0
  3. xrefkit/catalog_cli.py +57 -0
  4. xrefkit/cli.py +71 -0
  5. xrefkit/contracts.py +297 -0
  6. xrefkit/ctx.py +160 -0
  7. xrefkit/dashboard.py +1220 -0
  8. xrefkit/discovery.py +85 -0
  9. xrefkit/gate.py +428 -0
  10. xrefkit/goalstate.py +555 -0
  11. xrefkit/hashing.py +18 -0
  12. xrefkit/import_skill.py +469 -0
  13. xrefkit/instance.py +133 -0
  14. xrefkit/loaders.py +77 -0
  15. xrefkit/mcp/__init__.py +26 -0
  16. xrefkit/mcp/audit.py +168 -0
  17. xrefkit/mcp/bootstrap.py +337 -0
  18. xrefkit/mcp/catalog.py +2638 -0
  19. xrefkit/mcp/cli.py +173 -0
  20. xrefkit/mcp/client_cache.py +356 -0
  21. xrefkit/mcp/context_registry.py +277 -0
  22. xrefkit/mcp/contracts.py +243 -0
  23. xrefkit/mcp/dist.py +234 -0
  24. xrefkit/mcp/ownership.py +246 -0
  25. xrefkit/mcp/repository.py +217 -0
  26. xrefkit/mcp/schemas.py +349 -0
  27. xrefkit/mcp/server.py +773 -0
  28. xrefkit/mcp/startup_contract_pack.py +154 -0
  29. xrefkit/mcp_tools.py +47 -0
  30. xrefkit/models/__init__.py +41 -0
  31. xrefkit/models/common.py +185 -0
  32. xrefkit/models/effective_bundle.py +131 -0
  33. xrefkit/models/local_manifest.py +217 -0
  34. xrefkit/models/package_manifest.py +126 -0
  35. xrefkit/models/run_log.py +276 -0
  36. xrefkit/models/server_config.py +160 -0
  37. xrefkit/models/skill_definition.py +131 -0
  38. xrefkit/operations_cli.py +670 -0
  39. xrefkit/ownership.py +276 -0
  40. xrefkit/packmeta.py +289 -0
  41. xrefkit/registry.py +334 -0
  42. xrefkit/resolver.py +252 -0
  43. xrefkit/resource_provider.py +187 -0
  44. xrefkit/resources/base/contracts.json +178 -0
  45. xrefkit/resources/base/current.json +6 -0
  46. xrefkit/resources/base/generations/7a682a5272907354/contracts.json +178 -0
  47. xrefkit/resources/base/generations/7a682a5272907354/model_body.md +22 -0
  48. xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +178 -0
  49. xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +22 -0
  50. xrefkit/resources/base/model_body.md +22 -0
  51. xrefkit/runlog.py +45 -0
  52. xrefkit/skillmeta.py +1034 -0
  53. xrefkit/skillrun.py +2381 -0
  54. xrefkit/structure_catalog.py +199 -0
  55. xrefkit/tools/__init__.py +119 -0
  56. xrefkit/tools/__main__.py +4 -0
  57. xrefkit/v2_cli.py +130 -0
  58. xrefkit/workspace.py +117 -0
  59. xrefkit/xref.py +1048 -0
  60. xrefkit-0.3.0.dist-info/METADATA +203 -0
  61. xrefkit-0.3.0.dist-info/RECORD +65 -0
  62. xrefkit-0.3.0.dist-info/WHEEL +5 -0
  63. xrefkit-0.3.0.dist-info/entry_points.txt +2 -0
  64. xrefkit-0.3.0.dist-info/licenses/LICENSE +21 -0
  65. xrefkit-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,217 @@
1
+ """Pydantic models for Project Local manifests and domain skill wrappers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import Field, field_validator, model_validator
8
+
9
+ from .common import StrictModel, validate_non_empty, validate_package_id, validate_xid
10
+
11
+
12
+ class RequiredSkillPackage(StrictModel):
13
+ package_id: str
14
+ version: str
15
+
16
+ @field_validator("package_id")
17
+ @classmethod
18
+ def _validate_package_id(cls, value: str) -> str:
19
+ return validate_package_id(value)
20
+
21
+ @field_validator("version")
22
+ @classmethod
23
+ def _validate_version(cls, value: str) -> str:
24
+ return validate_non_empty(value, "version")
25
+
26
+
27
+ class LocalRequires(StrictModel):
28
+ xrefkit_core: str
29
+ skill_packages: list[RequiredSkillPackage]
30
+
31
+ @field_validator("xrefkit_core")
32
+ @classmethod
33
+ def _validate_core_constraint(cls, value: str) -> str:
34
+ return validate_non_empty(value, "xrefkit_core")
35
+
36
+ @field_validator("skill_packages")
37
+ @classmethod
38
+ def _validate_skill_packages(cls, values: list[RequiredSkillPackage]) -> list[RequiredSkillPackage]:
39
+ if not values:
40
+ raise ValueError("requires.skill_packages must not be empty")
41
+ package_ids = [item.package_id for item in values]
42
+ if len(package_ids) != len(set(package_ids)):
43
+ raise ValueError("requires.skill_packages contains duplicate package_id values")
44
+ return values
45
+
46
+
47
+ class LocalSkillMount(StrictModel):
48
+ path: str
49
+
50
+ @field_validator("path")
51
+ @classmethod
52
+ def _validate_path(cls, value: str) -> str:
53
+ return validate_non_empty(value, "path")
54
+
55
+
56
+ class LocalAssetMount(StrictModel):
57
+ id: str
58
+ xid: str
59
+ path: str
60
+
61
+ @field_validator("id", "path")
62
+ @classmethod
63
+ def _validate_non_empty(cls, value: str) -> str:
64
+ return validate_non_empty(value, "value")
65
+
66
+ @field_validator("xid")
67
+ @classmethod
68
+ def _validate_xid(cls, value: str) -> str:
69
+ return validate_xid(value)
70
+
71
+
72
+ class LocalMounts(StrictModel):
73
+ skills: list[LocalSkillMount] = Field(default_factory=list)
74
+ knowledge: list[LocalAssetMount] = Field(default_factory=list)
75
+ templates: list[LocalAssetMount] = Field(default_factory=list)
76
+ schemas: list[LocalAssetMount] = Field(default_factory=list)
77
+ review_axes: list[LocalAssetMount] = Field(default_factory=list)
78
+
79
+
80
+ class LocalMergePolicy(StrictModel):
81
+ can_extend_core: bool = True
82
+ can_weaken_core: bool = False
83
+ can_weaken_pack_contract: bool = False
84
+
85
+ @model_validator(mode="after")
86
+ def _enforce_mvp_policy(self) -> "LocalMergePolicy":
87
+ if self.can_weaken_core:
88
+ raise ValueError("Project Local cannot weaken Core")
89
+ if self.can_weaken_pack_contract:
90
+ raise ValueError("Project Local cannot weaken Pack contracts")
91
+ return self
92
+
93
+
94
+ class LocalManifest(StrictModel):
95
+ local_id: str
96
+ version: str
97
+ type: Literal["project_local"]
98
+ requires: LocalRequires
99
+ mounts: LocalMounts
100
+ merge_policy: LocalMergePolicy
101
+ metadata: dict[str, Any] | None = None
102
+ default_skill: str | None = None
103
+
104
+ @field_validator("local_id", "version")
105
+ @classmethod
106
+ def _validate_non_empty(cls, value: str) -> str:
107
+ return validate_non_empty(value, "value")
108
+
109
+ @model_validator(mode="after")
110
+ def _validate_unique_local_xids(self) -> "LocalManifest":
111
+ assets = self.mounts.knowledge + self.mounts.templates + self.mounts.schemas + self.mounts.review_axes
112
+ xids = [asset.xid for asset in assets]
113
+ if len(xids) != len(set(xids)):
114
+ raise ValueError("LocalManifest contains duplicate local XIDs")
115
+ return self
116
+
117
+
118
+ class ExtendsRef(StrictModel):
119
+ ref: str
120
+ xid: str
121
+ version: str
122
+ mode: Literal["contract_inheritance"]
123
+
124
+ @field_validator("ref", "version")
125
+ @classmethod
126
+ def _validate_non_empty(cls, value: str) -> str:
127
+ return validate_non_empty(value, "value")
128
+
129
+ @field_validator("xid")
130
+ @classmethod
131
+ def _validate_xid(cls, value: str) -> str:
132
+ return validate_xid(value)
133
+
134
+
135
+ class UsesRef(StrictModel):
136
+ ref: str
137
+ version: str
138
+ mode: Literal["supporting_skill"]
139
+
140
+ @field_validator("ref", "version")
141
+ @classmethod
142
+ def _validate_non_empty(cls, value: str) -> str:
143
+ return validate_non_empty(value, "value")
144
+
145
+
146
+ class IncludeRef(StrictModel):
147
+ xid: str
148
+
149
+ @field_validator("xid")
150
+ @classmethod
151
+ def _validate_xid(cls, value: str) -> str:
152
+ return validate_xid(value)
153
+
154
+
155
+ class InjectsSpec(StrictModel):
156
+ knowledge: list[str] = Field(default_factory=list)
157
+
158
+ @field_validator("knowledge")
159
+ @classmethod
160
+ def _validate_knowledge_xids(cls, values: list[str]) -> list[str]:
161
+ return [validate_xid(value) for value in values]
162
+
163
+
164
+ class OutputSpec(StrictModel):
165
+ template_xid: str | None = None
166
+ schema_xid: str | None = None
167
+
168
+ @field_validator("template_xid", "schema_xid")
169
+ @classmethod
170
+ def _validate_optional_xid(cls, value: str | None) -> str | None:
171
+ if value is None:
172
+ return None
173
+ return validate_xid(value)
174
+
175
+
176
+ class LocalSkillBinding(StrictModel):
177
+ extends: list[ExtendsRef]
178
+ includes: list[IncludeRef] = Field(default_factory=list)
179
+ uses: list[UsesRef] = Field(default_factory=list)
180
+ injects: InjectsSpec | None = None
181
+ output: OutputSpec | None = None
182
+ review_axes: list[str] = Field(default_factory=list)
183
+ required_outputs: list[str] = Field(default_factory=list)
184
+
185
+ @field_validator("extends")
186
+ @classmethod
187
+ def _validate_single_inheritance(cls, values: list[ExtendsRef]) -> list[ExtendsRef]:
188
+ if len(values) != 1:
189
+ raise ValueError("MVP supports exactly one extends entry")
190
+ return values
191
+
192
+ @field_validator("review_axes")
193
+ @classmethod
194
+ def _validate_review_axes(cls, values: list[str]) -> list[str]:
195
+ return [validate_xid(value) for value in values]
196
+
197
+ @field_validator("required_outputs")
198
+ @classmethod
199
+ def _validate_required_outputs(cls, values: list[str]) -> list[str]:
200
+ return [validate_non_empty(value, "required_output") for value in values]
201
+
202
+
203
+ class LocalDomainSkill(StrictModel):
204
+ skill_id: str
205
+ xid: str
206
+ type: Literal["domain_skill_wrapper"]
207
+ xrefkit: LocalSkillBinding
208
+
209
+ @field_validator("skill_id")
210
+ @classmethod
211
+ def _validate_skill_id(cls, value: str) -> str:
212
+ return validate_non_empty(value, "skill_id")
213
+
214
+ @field_validator("xid")
215
+ @classmethod
216
+ def _validate_xid(cls, value: str) -> str:
217
+ return validate_xid(value)
@@ -0,0 +1,126 @@
1
+ """Pydantic model for package_manifest.yaml."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import Field, field_validator, model_validator
8
+
9
+ from .common import StrictModel, validate_non_empty, validate_package_id, validate_xid
10
+
11
+
12
+ class PackageRequires(StrictModel):
13
+ xrefkit_core: str
14
+
15
+ @field_validator("xrefkit_core")
16
+ @classmethod
17
+ def _validate_constraint(cls, value: str) -> str:
18
+ return validate_non_empty(value, "xrefkit_core")
19
+
20
+
21
+ class ProvidedAsset(StrictModel):
22
+ id: str
23
+ xid: str
24
+ path: str
25
+
26
+ @field_validator("id", "path")
27
+ @classmethod
28
+ def _validate_non_empty(cls, value: str) -> str:
29
+ return validate_non_empty(value, "value")
30
+
31
+ @field_validator("xid")
32
+ @classmethod
33
+ def _validate_xid(cls, value: str) -> str:
34
+ return validate_xid(value)
35
+
36
+
37
+ class ProvidedSkill(ProvidedAsset):
38
+ contract_role: str | None = None
39
+ required_outputs: list[str] = Field(default_factory=list)
40
+ required_knowledge: list[str] = Field(default_factory=list)
41
+ required_review_axes: list[str] = Field(default_factory=list)
42
+ must_not: list[str] = Field(default_factory=list)
43
+
44
+ @field_validator("required_outputs", "required_knowledge", "required_review_axes", "must_not")
45
+ @classmethod
46
+ def _validate_string_list(cls, values: list[str]) -> list[str]:
47
+ return [validate_non_empty(value, "list item") for value in values]
48
+
49
+
50
+ class PackageProvides(StrictModel):
51
+ skills: list[ProvidedSkill]
52
+ fragments: list[ProvidedAsset] = Field(default_factory=list)
53
+ knowledge: list[ProvidedAsset] = Field(default_factory=list)
54
+ review_axes: list[ProvidedAsset] = Field(default_factory=list)
55
+ schemas: list[ProvidedAsset] = Field(default_factory=list)
56
+ templates: list[ProvidedAsset] = Field(default_factory=list)
57
+
58
+ @field_validator("skills")
59
+ @classmethod
60
+ def _require_skills(cls, values: list[ProvidedSkill]) -> list[ProvidedSkill]:
61
+ if not values:
62
+ raise ValueError("provides.skills must not be empty")
63
+ return values
64
+
65
+ @model_validator(mode="after")
66
+ def _validate_asset_ids(self) -> "PackageProvides":
67
+ for name in ("skills", "fragments", "knowledge", "review_axes", "schemas", "templates"):
68
+ assets = getattr(self, name)
69
+ ids = [asset.id for asset in assets]
70
+ if len(ids) != len(set(ids)):
71
+ raise ValueError(f"provides.{name} contains duplicate ids")
72
+ return self
73
+
74
+
75
+ class PackageContract(StrictModel):
76
+ can_be_extended_by_local_skill: bool = True
77
+ local_skill_can_weaken_contract: bool = False
78
+ local_can_weaken_required_outputs: bool = False
79
+ local_can_override_must_not: bool = False
80
+ requires_unknown_declaration: bool = True
81
+ requires_traceability: bool = True
82
+
83
+ @model_validator(mode="after")
84
+ def _enforce_mvp_policy(self) -> "PackageContract":
85
+ if self.local_skill_can_weaken_contract:
86
+ raise ValueError("MVP does not allow local skills to weaken package contracts")
87
+ if self.local_can_weaken_required_outputs:
88
+ raise ValueError("MVP does not allow local skills to weaken required outputs")
89
+ if self.local_can_override_must_not:
90
+ raise ValueError("MVP does not allow local skills to override must_not")
91
+ return self
92
+
93
+
94
+ class PackageManifest(StrictModel):
95
+ package_id: str
96
+ package_type: Literal["skill_package"]
97
+ version: str
98
+ requires: PackageRequires
99
+ provides: PackageProvides
100
+ contract: PackageContract
101
+ metadata: dict[str, Any] | None = None
102
+
103
+ @field_validator("package_id")
104
+ @classmethod
105
+ def _validate_package_id(cls, value: str) -> str:
106
+ return validate_package_id(value)
107
+
108
+ @field_validator("version")
109
+ @classmethod
110
+ def _validate_version(cls, value: str) -> str:
111
+ return validate_non_empty(value, "version")
112
+
113
+ @model_validator(mode="after")
114
+ def _validate_unique_xids(self) -> "PackageManifest":
115
+ assets = (
116
+ self.provides.skills
117
+ + self.provides.fragments
118
+ + self.provides.knowledge
119
+ + self.provides.review_axes
120
+ + self.provides.schemas
121
+ + self.provides.templates
122
+ )
123
+ xids = [asset.xid for asset in assets]
124
+ if len(xids) != len(set(xids)):
125
+ raise ValueError("package manifest contains duplicate XIDs")
126
+ return self
@@ -0,0 +1,276 @@
1
+ """Pydantic models for canonical JSONL run log events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import Annotated, Literal, Union
7
+
8
+ from pydantic import Field, field_validator, model_validator
9
+
10
+ from .common import SourceTraceEntry, StrictModel, XidLoadedRef, validate_content_hash, validate_non_empty, validate_xid
11
+
12
+
13
+ class Requester(StrictModel):
14
+ type: Literal["client_ip", "user_id"]
15
+ client_ip: str | None = None
16
+ user_id: str | None = None
17
+ identity_assurance: str
18
+
19
+ @field_validator("identity_assurance")
20
+ @classmethod
21
+ def _validate_identity_assurance(cls, value: str) -> str:
22
+ return validate_non_empty(value, "identity_assurance")
23
+
24
+ @model_validator(mode="after")
25
+ def _validate_identity(self) -> "Requester":
26
+ if self.type == "client_ip" and not self.client_ip:
27
+ raise ValueError("requester.client_ip is required when requester.type='client_ip'")
28
+ if self.type == "user_id" and not self.user_id:
29
+ raise ValueError("requester.user_id is required when requester.type='user_id'")
30
+ return self
31
+
32
+
33
+ class Actor(StrictModel):
34
+ type: str
35
+ client_name: str | None = None
36
+ session_id: str | None = None
37
+
38
+ @field_validator("type")
39
+ @classmethod
40
+ def _validate_type(cls, value: str) -> str:
41
+ return validate_non_empty(value, "type")
42
+
43
+
44
+ class RunRequest(StrictModel):
45
+ operation: str
46
+ skill_id: str | None = None
47
+
48
+ @field_validator("operation")
49
+ @classmethod
50
+ def _validate_operation(cls, value: str) -> str:
51
+ return validate_non_empty(value, "operation")
52
+
53
+
54
+ class RunLogEventBase(StrictModel):
55
+ run_id: str
56
+ event_type: str
57
+ timestamp: datetime
58
+
59
+ @field_validator("run_id")
60
+ @classmethod
61
+ def _validate_run_id(cls, value: str) -> str:
62
+ return validate_non_empty(value, "run_id")
63
+
64
+
65
+ class RunStartEvent(RunLogEventBase):
66
+ event_type: Literal["run.start"]
67
+ requester: Requester
68
+ actor: Actor | None = None
69
+ request: RunRequest
70
+
71
+
72
+ class ContextResolvedEvent(RunLogEventBase):
73
+ event_type: Literal["context.resolved"]
74
+ effective_skill_id: str
75
+ resolution_mode: Literal["entry", "branch", "full"]
76
+ source_trace: list[SourceTraceEntry]
77
+ bundle_hash: str | None = None
78
+
79
+ @field_validator("effective_skill_id")
80
+ @classmethod
81
+ def _validate_effective_skill_id(cls, value: str) -> str:
82
+ return validate_non_empty(value, "effective_skill_id")
83
+
84
+ @field_validator("bundle_hash")
85
+ @classmethod
86
+ def _validate_bundle_hash(cls, value: str | None) -> str | None:
87
+ if value is None:
88
+ return None
89
+ return validate_content_hash(value)
90
+
91
+
92
+ class XidsReferencedEvent(RunLogEventBase):
93
+ event_type: Literal["xids.referenced"]
94
+ referenced_xids: list[str]
95
+
96
+ @field_validator("referenced_xids")
97
+ @classmethod
98
+ def _validate_xids(cls, values: list[str]) -> list[str]:
99
+ return [validate_xid(value) for value in values]
100
+
101
+
102
+ class XidsLoadedEvent(RunLogEventBase):
103
+ event_type: Literal["xids.loaded"]
104
+ loaded_xids: list[XidLoadedRef]
105
+
106
+
107
+ class XidsUsedEvent(RunLogEventBase):
108
+ event_type: Literal["xids.used"]
109
+ used_xids: list[str]
110
+
111
+ @field_validator("used_xids")
112
+ @classmethod
113
+ def _validate_xids(cls, values: list[str]) -> list[str]:
114
+ return [validate_xid(value) for value in values]
115
+
116
+
117
+ class UnknownsReportedEvent(RunLogEventBase):
118
+ event_type: Literal["unknowns.reported"]
119
+ unknowns: list[str]
120
+
121
+ @field_validator("unknowns")
122
+ @classmethod
123
+ def _validate_unknowns(cls, values: list[str]) -> list[str]:
124
+ return [validate_non_empty(value, "unknown") for value in values]
125
+
126
+
127
+ class AssumptionsReportedEvent(RunLogEventBase):
128
+ event_type: Literal["assumptions.reported"]
129
+ assumptions: list[str]
130
+
131
+ @field_validator("assumptions")
132
+ @classmethod
133
+ def _validate_assumptions(cls, values: list[str]) -> list[str]:
134
+ return [validate_non_empty(value, "assumption") for value in values]
135
+
136
+
137
+ class BranchLoadedEvent(RunLogEventBase):
138
+ event_type: Literal["branch.loaded"]
139
+ branch_xid: str
140
+ loaded_xids: list[XidLoadedRef] = Field(default_factory=list)
141
+
142
+ @field_validator("branch_xid")
143
+ @classmethod
144
+ def _validate_branch_xid(cls, value: str) -> str:
145
+ return validate_xid(value)
146
+
147
+
148
+ class ConflictDetectedEvent(RunLogEventBase):
149
+ event_type: Literal["conflict.detected"]
150
+ severity: Literal["error", "warning", "info"]
151
+ code: str
152
+ message: str
153
+ xids: list[str] = Field(default_factory=list)
154
+
155
+ @field_validator("code", "message")
156
+ @classmethod
157
+ def _validate_non_empty(cls, value: str) -> str:
158
+ return validate_non_empty(value, "value")
159
+
160
+ @field_validator("xids")
161
+ @classmethod
162
+ def _validate_xids(cls, values: list[str]) -> list[str]:
163
+ return [validate_xid(value) for value in values]
164
+
165
+
166
+ class RunResult(StrictModel):
167
+ status: Literal["completed", "completed_with_unknowns", "failed", "blocked"]
168
+ contract_passed: bool
169
+ required_outputs_present: bool | None = None
170
+ unknowns_count: int | None = None
171
+ assumptions_count: int | None = None
172
+ message: str | None = None
173
+
174
+ @field_validator("unknowns_count", "assumptions_count")
175
+ @classmethod
176
+ def _validate_counts(cls, value: int | None) -> int | None:
177
+ if value is not None and value < 0:
178
+ raise ValueError("counts must be zero or greater")
179
+ return value
180
+
181
+
182
+ class RunCompleteEvent(RunLogEventBase):
183
+ event_type: Literal["run.complete"]
184
+ result: RunResult
185
+
186
+
187
+ RunLogEvent = Annotated[
188
+ Union[
189
+ RunStartEvent,
190
+ ContextResolvedEvent,
191
+ XidsReferencedEvent,
192
+ XidsLoadedEvent,
193
+ XidsUsedEvent,
194
+ UnknownsReportedEvent,
195
+ AssumptionsReportedEvent,
196
+ BranchLoadedEvent,
197
+ ConflictDetectedEvent,
198
+ RunCompleteEvent,
199
+ ],
200
+ Field(discriminator="event_type"),
201
+ ]
202
+
203
+
204
+ class RunLogAggregate(StrictModel):
205
+ run_id: str
206
+ events: list[RunLogEvent]
207
+
208
+ @field_validator("run_id")
209
+ @classmethod
210
+ def _validate_run_id(cls, value: str) -> str:
211
+ return validate_non_empty(value, "run_id")
212
+
213
+ @field_validator("events")
214
+ @classmethod
215
+ def _validate_events_not_empty(cls, values: list[RunLogEvent]) -> list[RunLogEvent]:
216
+ if not values:
217
+ raise ValueError("events must not be empty")
218
+ return values
219
+
220
+ @model_validator(mode="after")
221
+ def _validate_run_consistency(self) -> "RunLogAggregate":
222
+ for event in self.events:
223
+ if event.run_id != self.run_id:
224
+ raise ValueError("all run log events must share the aggregate run_id")
225
+
226
+ if not any(isinstance(event, RunStartEvent) for event in self.events):
227
+ raise ValueError("run.start event is required")
228
+
229
+ complete_count = sum(1 for event in self.events if isinstance(event, RunCompleteEvent))
230
+ if complete_count > 1:
231
+ raise ValueError("run.complete may appear at most once")
232
+
233
+ timestamps = [event.timestamp for event in self.events]
234
+ if timestamps != sorted(timestamps):
235
+ raise ValueError("run log events must be ordered by timestamp")
236
+
237
+ loaded_xids: set[str] = set()
238
+ used_xids: set[str] = set()
239
+ for event in self.events:
240
+ if isinstance(event, XidsLoadedEvent):
241
+ loaded_xids.update(item.xid for item in event.loaded_xids)
242
+ elif isinstance(event, BranchLoadedEvent):
243
+ loaded_xids.update(item.xid for item in event.loaded_xids)
244
+ elif isinstance(event, XidsUsedEvent):
245
+ used_xids.update(event.used_xids)
246
+
247
+ missing = sorted(used_xids - loaded_xids)
248
+ if missing:
249
+ raise ValueError(f"used_xids must be a subset of loaded_xids; missing: {missing}")
250
+ return self
251
+
252
+ @property
253
+ def referenced_xids(self) -> set[str]:
254
+ values: set[str] = set()
255
+ for event in self.events:
256
+ if isinstance(event, XidsReferencedEvent):
257
+ values.update(event.referenced_xids)
258
+ return values
259
+
260
+ @property
261
+ def loaded_xids(self) -> set[str]:
262
+ values: set[str] = set()
263
+ for event in self.events:
264
+ if isinstance(event, XidsLoadedEvent):
265
+ values.update(item.xid for item in event.loaded_xids)
266
+ elif isinstance(event, BranchLoadedEvent):
267
+ values.update(item.xid for item in event.loaded_xids)
268
+ return values
269
+
270
+ @property
271
+ def used_xids(self) -> set[str]:
272
+ values: set[str] = set()
273
+ for event in self.events:
274
+ if isinstance(event, XidsUsedEvent):
275
+ values.update(event.used_xids)
276
+ return values