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.
- xrefkit/__init__.py +5 -0
- xrefkit/__main__.py +5 -0
- xrefkit/catalog_cli.py +57 -0
- xrefkit/cli.py +71 -0
- xrefkit/contracts.py +297 -0
- xrefkit/ctx.py +160 -0
- xrefkit/dashboard.py +1220 -0
- xrefkit/discovery.py +85 -0
- xrefkit/gate.py +428 -0
- xrefkit/goalstate.py +555 -0
- xrefkit/hashing.py +18 -0
- xrefkit/import_skill.py +469 -0
- xrefkit/instance.py +133 -0
- xrefkit/loaders.py +77 -0
- xrefkit/mcp/__init__.py +26 -0
- xrefkit/mcp/audit.py +168 -0
- xrefkit/mcp/bootstrap.py +337 -0
- xrefkit/mcp/catalog.py +2638 -0
- xrefkit/mcp/cli.py +173 -0
- xrefkit/mcp/client_cache.py +356 -0
- xrefkit/mcp/context_registry.py +277 -0
- xrefkit/mcp/contracts.py +243 -0
- xrefkit/mcp/dist.py +234 -0
- xrefkit/mcp/ownership.py +246 -0
- xrefkit/mcp/repository.py +217 -0
- xrefkit/mcp/schemas.py +349 -0
- xrefkit/mcp/server.py +773 -0
- xrefkit/mcp/startup_contract_pack.py +154 -0
- xrefkit/mcp_tools.py +47 -0
- xrefkit/models/__init__.py +41 -0
- xrefkit/models/common.py +185 -0
- xrefkit/models/effective_bundle.py +131 -0
- xrefkit/models/local_manifest.py +217 -0
- xrefkit/models/package_manifest.py +126 -0
- xrefkit/models/run_log.py +276 -0
- xrefkit/models/server_config.py +160 -0
- xrefkit/models/skill_definition.py +131 -0
- xrefkit/operations_cli.py +670 -0
- xrefkit/ownership.py +276 -0
- xrefkit/packmeta.py +289 -0
- xrefkit/registry.py +334 -0
- xrefkit/resolver.py +252 -0
- xrefkit/resource_provider.py +187 -0
- xrefkit/resources/base/contracts.json +178 -0
- xrefkit/resources/base/current.json +6 -0
- xrefkit/resources/base/generations/7a682a5272907354/contracts.json +178 -0
- xrefkit/resources/base/generations/7a682a5272907354/model_body.md +22 -0
- xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +178 -0
- xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +22 -0
- xrefkit/resources/base/model_body.md +22 -0
- xrefkit/runlog.py +45 -0
- xrefkit/skillmeta.py +1034 -0
- xrefkit/skillrun.py +2381 -0
- xrefkit/structure_catalog.py +199 -0
- xrefkit/tools/__init__.py +119 -0
- xrefkit/tools/__main__.py +4 -0
- xrefkit/v2_cli.py +130 -0
- xrefkit/workspace.py +117 -0
- xrefkit/xref.py +1048 -0
- xrefkit-0.3.0.dist-info/METADATA +203 -0
- xrefkit-0.3.0.dist-info/RECORD +65 -0
- xrefkit-0.3.0.dist-info/WHEEL +5 -0
- xrefkit-0.3.0.dist-info/entry_points.txt +2 -0
- xrefkit-0.3.0.dist-info/licenses/LICENSE +21 -0
- 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
|