permea-core 0.1.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.
- permea_core/__init__.py +9 -0
- permea_core/acquisition/__init__.py +39 -0
- permea_core/acquisition/manifests.py +470 -0
- permea_core/benchmarks/__init__.py +73 -0
- permea_core/benchmarks/cards.py +226 -0
- permea_core/benchmarks/evidence_cards.py +201 -0
- permea_core/benchmarks/output_package.py +212 -0
- permea_core/benchmarks/registry.py +246 -0
- permea_core/cli/__init__.py +5 -0
- permea_core/cli/_format.py +49 -0
- permea_core/cli/bench.py +117 -0
- permea_core/cli/main.py +35 -0
- permea_core/cluster/__init__.py +27 -0
- permea_core/cluster/identity.py +248 -0
- permea_core/consistency/__init__.py +1 -0
- permea_core/consistency/artifacts.py +264 -0
- permea_core/contracts/__init__.py +8 -0
- permea_core/contracts/warnings.py +297 -0
- permea_core/data/__init__.py +5 -0
- permea_core/data/contracts.py +43 -0
- permea_core/datasets/__init__.py +41 -0
- permea_core/datasets/cards.py +457 -0
- permea_core/datasets/load.py +30 -0
- permea_core/demo/__init__.py +1 -0
- permea_core/demo/packet.py +291 -0
- permea_core/diagnose/__init__.py +35 -0
- permea_core/diagnose/rules.py +293 -0
- permea_core/dry_run/__init__.py +15 -0
- permea_core/dry_run/orchestrator.py +269 -0
- permea_core/eval/__init__.py +5 -0
- permea_core/eval/bootstrap.py +71 -0
- permea_core/eval/engine.py +180 -0
- permea_core/eval/metrics.py +43 -0
- permea_core/eval/run.py +234 -0
- permea_core/evaluation/__init__.py +2 -0
- permea_core/evaluation/bundle.py +335 -0
- permea_core/evidence/__init__.py +1 -0
- permea_core/evidence/matrix.py +349 -0
- permea_core/generation/__init__.py +13 -0
- permea_core/generation/artifacts.py +96 -0
- permea_core/index/__init__.py +15 -0
- permea_core/index/artifact_index.py +224 -0
- permea_core/provenance/__init__.py +5 -0
- permea_core/provenance/run_manifest.py +35 -0
- permea_core/represent/__init__.py +19 -0
- permea_core/represent/physchem.py +61 -0
- permea_core/reproducibility/bundle.py +269 -0
- permea_core/review_packets/__init__.py +19 -0
- permea_core/review_packets/bundle_completeness.py +117 -0
- permea_core/review_packets/coverage.py +102 -0
- permea_core/review_packets/loop_readiness.py +152 -0
- permea_core/review_packets/packets.py +483 -0
- permea_core/runners/__init__.py +5 -0
- permea_core/runners/base.py +34 -0
- permea_core/runs/__init__.py +1 -0
- permea_core/runs/manifests.py +435 -0
- permea_core/sources/__init__.py +27 -0
- permea_core/sources/registry.py +233 -0
- permea_core/specs/__init__.py +2 -0
- permea_core/specs/registry.py +119 -0
- permea_core/surface/evidence_surface.py +276 -0
- permea_core/validation/__init__.py +21 -0
- permea_core/validation/artifact_validator.py +585 -0
- permea_core/validation/artifacts.py +104 -0
- permea_core-0.1.0.dist-info/METADATA +574 -0
- permea_core-0.1.0.dist-info/RECORD +94 -0
- permea_core-0.1.0.dist-info/WHEEL +5 -0
- permea_core-0.1.0.dist-info/entry_points.txt +3 -0
- permea_core-0.1.0.dist-info/licenses/LICENSE +201 -0
- permea_core-0.1.0.dist-info/top_level.txt +3 -0
- permea_explain/__init__.py +27 -0
- permea_explain/extract.py +1304 -0
- permea_explain/extract_prompt.py +111 -0
- permea_explain/guardrails.py +267 -0
- permea_explain/narrate.py +74 -0
- permea_explain/prompt.py +84 -0
- permea_explain/providers/__init__.py +37 -0
- permea_explain/providers/anthropic.py +29 -0
- permea_explain/providers/base.py +46 -0
- permea_explain/providers/configured.py +167 -0
- permea_explain/providers/openai.py +29 -0
- permea_explain/schemas/extract.schema.json +490 -0
- permea_explain/schemas/label_schema.schema.json +160 -0
- permea_explain/source.py +48 -0
- permea_explain/violations.py +145 -0
- permea_ui/__init__.py +12 -0
- permea_ui/__main__.py +79 -0
- permea_ui/app.py +223 -0
- permea_ui/fixtures.py +106 -0
- permea_ui/jobs.py +90 -0
- permea_ui/runner.py +234 -0
- permea_ui/static/app.js +294 -0
- permea_ui/static/index.html +88 -0
- permea_ui/static/style.css +208 -0
permea_core/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Acquisition manifest validation and generation surfaces for Permea Core."""
|
|
2
|
+
|
|
3
|
+
from .manifests import (
|
|
4
|
+
ALLOWED_ACQUISITION_MODES,
|
|
5
|
+
ALLOWED_ACQUISITION_STATUSES,
|
|
6
|
+
ALLOWED_REDISTRIBUTION_STATUSES,
|
|
7
|
+
DEFAULT_ACQUISITION_MANIFEST_DIR,
|
|
8
|
+
DEFAULT_GENERATED_ACQUISITION_MANIFEST_DIR,
|
|
9
|
+
REQUIRED_ACQUISITION_MANIFEST_FIELDS,
|
|
10
|
+
AcquisitionManifestBatchResult,
|
|
11
|
+
AcquisitionManifestGenerationResult,
|
|
12
|
+
AcquisitionManifestValidationError,
|
|
13
|
+
AcquisitionManifestValidationResult,
|
|
14
|
+
generate_acquisition_manifest_file,
|
|
15
|
+
generate_acquisition_manifests,
|
|
16
|
+
load_acquisition_manifest_yaml,
|
|
17
|
+
render_acquisition_manifest,
|
|
18
|
+
validate_acquisition_manifest,
|
|
19
|
+
validate_acquisition_manifest_file,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ALLOWED_ACQUISITION_MODES",
|
|
24
|
+
"ALLOWED_ACQUISITION_STATUSES",
|
|
25
|
+
"ALLOWED_REDISTRIBUTION_STATUSES",
|
|
26
|
+
"DEFAULT_ACQUISITION_MANIFEST_DIR",
|
|
27
|
+
"DEFAULT_GENERATED_ACQUISITION_MANIFEST_DIR",
|
|
28
|
+
"REQUIRED_ACQUISITION_MANIFEST_FIELDS",
|
|
29
|
+
"AcquisitionManifestBatchResult",
|
|
30
|
+
"AcquisitionManifestGenerationResult",
|
|
31
|
+
"AcquisitionManifestValidationError",
|
|
32
|
+
"AcquisitionManifestValidationResult",
|
|
33
|
+
"generate_acquisition_manifest_file",
|
|
34
|
+
"generate_acquisition_manifests",
|
|
35
|
+
"load_acquisition_manifest_yaml",
|
|
36
|
+
"render_acquisition_manifest",
|
|
37
|
+
"validate_acquisition_manifest",
|
|
38
|
+
"validate_acquisition_manifest_file",
|
|
39
|
+
]
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Acquisition manifest YAML validation and Markdown generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import yaml
|
|
11
|
+
except ImportError: # pragma: no cover - exercised only without PyYAML installed.
|
|
12
|
+
yaml = None # type: ignore[assignment]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_ACQUISITION_MANIFEST_DIR = Path("acquisition_manifests")
|
|
16
|
+
DEFAULT_GENERATED_ACQUISITION_MANIFEST_DIR = Path(
|
|
17
|
+
"docs/examples/generated/acquisition_manifests"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
REQUIRED_ACQUISITION_MANIFEST_FIELDS = frozenset(
|
|
21
|
+
{
|
|
22
|
+
"manifest_id",
|
|
23
|
+
"dataset_id",
|
|
24
|
+
"source_ids",
|
|
25
|
+
"benchmark_ids",
|
|
26
|
+
"acquisition_mode",
|
|
27
|
+
"acquisition_status",
|
|
28
|
+
"redistribution_status",
|
|
29
|
+
"expected_local_outputs",
|
|
30
|
+
"provenance_requirements",
|
|
31
|
+
"license_review_required",
|
|
32
|
+
"manual_review_required",
|
|
33
|
+
"failure_modes",
|
|
34
|
+
"fallback_strategy",
|
|
35
|
+
"claim_boundary",
|
|
36
|
+
"next_action",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
ALLOWED_ACQUISITION_MODES = frozenset(
|
|
41
|
+
{
|
|
42
|
+
"manual-source-card",
|
|
43
|
+
"public-download-to-verify",
|
|
44
|
+
"api-metadata-to-verify",
|
|
45
|
+
"literature-metadata",
|
|
46
|
+
"no-redistribution-source-card-only",
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
ALLOWED_ACQUISITION_STATUSES = frozenset(
|
|
51
|
+
{
|
|
52
|
+
"source-carded",
|
|
53
|
+
"access-to-verify",
|
|
54
|
+
"license-to-verify",
|
|
55
|
+
"acquisition-planned",
|
|
56
|
+
"acquisition-script-planned",
|
|
57
|
+
"no-redistribution-source-card-only",
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
ALLOWED_REDISTRIBUTION_STATUSES = frozenset(
|
|
62
|
+
{
|
|
63
|
+
"not-reviewed",
|
|
64
|
+
"not-confirmed",
|
|
65
|
+
"no-redistribution",
|
|
66
|
+
"metadata-only",
|
|
67
|
+
"redistribution-to-verify",
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
NON_CLAIMS = (
|
|
72
|
+
"no dataset downloaded",
|
|
73
|
+
"no acquisition executed",
|
|
74
|
+
"no redistribution rights confirmed",
|
|
75
|
+
"no wet-lab validation by Permea",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class AcquisitionManifestValidationError:
|
|
81
|
+
"""Structured validation error for one acquisition manifest."""
|
|
82
|
+
|
|
83
|
+
entry: str
|
|
84
|
+
field: str
|
|
85
|
+
message: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True, slots=True)
|
|
89
|
+
class AcquisitionManifestValidationResult:
|
|
90
|
+
"""Validation outcome for one acquisition manifest YAML file."""
|
|
91
|
+
|
|
92
|
+
path: Path
|
|
93
|
+
manifest_id: str
|
|
94
|
+
errors: tuple[AcquisitionManifestValidationError, ...] = field(default_factory=tuple)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def passed(self) -> bool:
|
|
98
|
+
"""Return True when the acquisition manifest has no validation errors."""
|
|
99
|
+
return not self.errors
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class AcquisitionManifestGenerationResult:
|
|
104
|
+
"""Structured result for one generated acquisition manifest file."""
|
|
105
|
+
|
|
106
|
+
input_path: Path
|
|
107
|
+
output_path: Path
|
|
108
|
+
manifest_id: str
|
|
109
|
+
passed: bool
|
|
110
|
+
message: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True, slots=True)
|
|
114
|
+
class AcquisitionManifestBatchResult:
|
|
115
|
+
"""Structured result for generating multiple acquisition manifest files."""
|
|
116
|
+
|
|
117
|
+
input_dir: Path
|
|
118
|
+
output_dir: Path
|
|
119
|
+
generated: tuple[AcquisitionManifestGenerationResult, ...]
|
|
120
|
+
passed: bool
|
|
121
|
+
message: str
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def load_acquisition_manifest_yaml(path: str | Path) -> Any:
|
|
125
|
+
"""Load a local acquisition manifest YAML file."""
|
|
126
|
+
if yaml is None:
|
|
127
|
+
raise RuntimeError(
|
|
128
|
+
"PyYAML is required to validate acquisition manifest YAML files. "
|
|
129
|
+
"Install PyYAML or run in the project environment that provides it."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
manifest_path = Path(path)
|
|
133
|
+
with manifest_path.open("r", encoding="utf-8") as file:
|
|
134
|
+
return yaml.safe_load(file)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def validate_acquisition_manifest(
|
|
138
|
+
manifest: dict[str, Any],
|
|
139
|
+
) -> list[AcquisitionManifestValidationError]:
|
|
140
|
+
"""Validate one acquisition manifest mapping and return structured errors."""
|
|
141
|
+
name = _entry_name(manifest)
|
|
142
|
+
errors: list[AcquisitionManifestValidationError] = []
|
|
143
|
+
|
|
144
|
+
missing = sorted(REQUIRED_ACQUISITION_MANIFEST_FIELDS.difference(manifest))
|
|
145
|
+
for field_name in missing:
|
|
146
|
+
errors.append(
|
|
147
|
+
AcquisitionManifestValidationError(
|
|
148
|
+
entry=name,
|
|
149
|
+
field=field_name,
|
|
150
|
+
message="Required field is missing.",
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
_validate_enum(
|
|
155
|
+
errors=errors,
|
|
156
|
+
entry=name,
|
|
157
|
+
field="acquisition_mode",
|
|
158
|
+
value=manifest.get("acquisition_mode"),
|
|
159
|
+
allowed=ALLOWED_ACQUISITION_MODES,
|
|
160
|
+
)
|
|
161
|
+
_validate_enum(
|
|
162
|
+
errors=errors,
|
|
163
|
+
entry=name,
|
|
164
|
+
field="acquisition_status",
|
|
165
|
+
value=manifest.get("acquisition_status"),
|
|
166
|
+
allowed=ALLOWED_ACQUISITION_STATUSES,
|
|
167
|
+
)
|
|
168
|
+
_validate_enum(
|
|
169
|
+
errors=errors,
|
|
170
|
+
entry=name,
|
|
171
|
+
field="redistribution_status",
|
|
172
|
+
value=manifest.get("redistribution_status"),
|
|
173
|
+
allowed=ALLOWED_REDISTRIBUTION_STATUSES,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
for field_name in (
|
|
177
|
+
"source_ids",
|
|
178
|
+
"benchmark_ids",
|
|
179
|
+
"expected_local_outputs",
|
|
180
|
+
"provenance_requirements",
|
|
181
|
+
"failure_modes",
|
|
182
|
+
"fallback_strategy",
|
|
183
|
+
):
|
|
184
|
+
value = manifest.get(field_name)
|
|
185
|
+
if value is not None and not isinstance(value, list):
|
|
186
|
+
errors.append(
|
|
187
|
+
AcquisitionManifestValidationError(
|
|
188
|
+
entry=name,
|
|
189
|
+
field=field_name,
|
|
190
|
+
message=f"{field_name} must be a list.",
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
for field_name in ("license_review_required", "manual_review_required"):
|
|
195
|
+
value = manifest.get(field_name)
|
|
196
|
+
if value is not None and not isinstance(value, bool):
|
|
197
|
+
errors.append(
|
|
198
|
+
AcquisitionManifestValidationError(
|
|
199
|
+
entry=name,
|
|
200
|
+
field=field_name,
|
|
201
|
+
message=f"{field_name} must be a boolean.",
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return errors
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def validate_acquisition_manifest_file(
|
|
209
|
+
path: str | Path,
|
|
210
|
+
) -> AcquisitionManifestValidationResult:
|
|
211
|
+
"""Validate one acquisition manifest YAML file."""
|
|
212
|
+
manifest_path = Path(path)
|
|
213
|
+
try:
|
|
214
|
+
payload = load_acquisition_manifest_yaml(manifest_path)
|
|
215
|
+
except Exception as exc: # noqa: BLE001 - CLI should report load failures clearly.
|
|
216
|
+
return AcquisitionManifestValidationResult(
|
|
217
|
+
path=manifest_path,
|
|
218
|
+
manifest_id="<invalid>",
|
|
219
|
+
errors=(
|
|
220
|
+
AcquisitionManifestValidationError(
|
|
221
|
+
entry="<file>",
|
|
222
|
+
field="<load>",
|
|
223
|
+
message=str(exc),
|
|
224
|
+
),
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if not isinstance(payload, dict):
|
|
229
|
+
return AcquisitionManifestValidationResult(
|
|
230
|
+
path=manifest_path,
|
|
231
|
+
manifest_id="<invalid>",
|
|
232
|
+
errors=(
|
|
233
|
+
AcquisitionManifestValidationError(
|
|
234
|
+
entry="<file>",
|
|
235
|
+
field="<payload>",
|
|
236
|
+
message="Acquisition manifest YAML must contain one mapping.",
|
|
237
|
+
),
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return AcquisitionManifestValidationResult(
|
|
242
|
+
path=manifest_path,
|
|
243
|
+
manifest_id=_entry_name(payload),
|
|
244
|
+
errors=tuple(validate_acquisition_manifest(payload)),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def render_acquisition_manifest(manifest: dict[str, Any]) -> str:
|
|
249
|
+
"""Render one validated acquisition manifest as deterministic Markdown."""
|
|
250
|
+
manifest_id = _required_string(manifest, "manifest_id")
|
|
251
|
+
dataset_id = _required_string(manifest, "dataset_id")
|
|
252
|
+
|
|
253
|
+
sections = [
|
|
254
|
+
f"# Acquisition Manifest: {manifest_id}",
|
|
255
|
+
"",
|
|
256
|
+
"> Generated from Permea acquisition-manifest metadata. This public-safe example records planning metadata only: no dataset downloaded, no acquisition executed, no redistribution rights confirmed, and no wet-lab validation by Permea.",
|
|
257
|
+
"",
|
|
258
|
+
"## Manifest ID",
|
|
259
|
+
"",
|
|
260
|
+
manifest_id,
|
|
261
|
+
"",
|
|
262
|
+
"## Dataset ID",
|
|
263
|
+
"",
|
|
264
|
+
dataset_id,
|
|
265
|
+
"",
|
|
266
|
+
"## Source IDs",
|
|
267
|
+
"",
|
|
268
|
+
_render_list(manifest.get("source_ids")),
|
|
269
|
+
"",
|
|
270
|
+
"## Benchmark IDs",
|
|
271
|
+
"",
|
|
272
|
+
_render_list(manifest.get("benchmark_ids")),
|
|
273
|
+
"",
|
|
274
|
+
"## Acquisition Mode",
|
|
275
|
+
"",
|
|
276
|
+
_required_string(manifest, "acquisition_mode"),
|
|
277
|
+
"",
|
|
278
|
+
"## Acquisition Status",
|
|
279
|
+
"",
|
|
280
|
+
_required_string(manifest, "acquisition_status"),
|
|
281
|
+
"",
|
|
282
|
+
"## Redistribution Status",
|
|
283
|
+
"",
|
|
284
|
+
_required_string(manifest, "redistribution_status"),
|
|
285
|
+
"",
|
|
286
|
+
"## Expected Local Outputs",
|
|
287
|
+
"",
|
|
288
|
+
_render_list(manifest.get("expected_local_outputs")),
|
|
289
|
+
"",
|
|
290
|
+
"## Provenance Requirements",
|
|
291
|
+
"",
|
|
292
|
+
_render_list(manifest.get("provenance_requirements")),
|
|
293
|
+
"",
|
|
294
|
+
"## Review Requirements",
|
|
295
|
+
"",
|
|
296
|
+
f"- license_review_required: {manifest.get('license_review_required')}",
|
|
297
|
+
f"- manual_review_required: {manifest.get('manual_review_required')}",
|
|
298
|
+
"",
|
|
299
|
+
"## Failure Modes",
|
|
300
|
+
"",
|
|
301
|
+
_render_list(manifest.get("failure_modes")),
|
|
302
|
+
"",
|
|
303
|
+
"## Fallback Strategy",
|
|
304
|
+
"",
|
|
305
|
+
_render_list(manifest.get("fallback_strategy")),
|
|
306
|
+
"",
|
|
307
|
+
"## Explicit Non-Claims",
|
|
308
|
+
"",
|
|
309
|
+
_render_list(list(NON_CLAIMS)),
|
|
310
|
+
"",
|
|
311
|
+
"## Claim Boundary",
|
|
312
|
+
"",
|
|
313
|
+
_required_string(manifest, "claim_boundary"),
|
|
314
|
+
"",
|
|
315
|
+
"## Next Action",
|
|
316
|
+
"",
|
|
317
|
+
_required_string(manifest, "next_action"),
|
|
318
|
+
"",
|
|
319
|
+
]
|
|
320
|
+
return "\n".join(sections)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def generate_acquisition_manifest_file(
|
|
324
|
+
input_path: str | Path,
|
|
325
|
+
output_path: str | Path,
|
|
326
|
+
) -> AcquisitionManifestGenerationResult:
|
|
327
|
+
"""Validate one acquisition manifest YAML file and write its Markdown."""
|
|
328
|
+
manifest_path = Path(input_path)
|
|
329
|
+
output = Path(output_path)
|
|
330
|
+
|
|
331
|
+
validation = validate_acquisition_manifest_file(manifest_path)
|
|
332
|
+
if not validation.passed:
|
|
333
|
+
return AcquisitionManifestGenerationResult(
|
|
334
|
+
input_path=manifest_path,
|
|
335
|
+
output_path=output,
|
|
336
|
+
manifest_id=validation.manifest_id,
|
|
337
|
+
passed=False,
|
|
338
|
+
message=_format_validation_failure(validation.errors),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
payload = load_acquisition_manifest_yaml(manifest_path)
|
|
342
|
+
if not isinstance(payload, dict):
|
|
343
|
+
return AcquisitionManifestGenerationResult(
|
|
344
|
+
input_path=manifest_path,
|
|
345
|
+
output_path=output,
|
|
346
|
+
manifest_id="<invalid>",
|
|
347
|
+
passed=False,
|
|
348
|
+
message="Acquisition manifest YAML must contain one mapping.",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
352
|
+
output.write_text(render_acquisition_manifest(payload), encoding="utf-8")
|
|
353
|
+
return AcquisitionManifestGenerationResult(
|
|
354
|
+
input_path=manifest_path,
|
|
355
|
+
output_path=output,
|
|
356
|
+
manifest_id=_required_string(payload, "manifest_id"),
|
|
357
|
+
passed=True,
|
|
358
|
+
message="Generated acquisition manifest.",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def generate_acquisition_manifests(
|
|
363
|
+
input_dir: str | Path,
|
|
364
|
+
output_dir: str | Path,
|
|
365
|
+
) -> AcquisitionManifestBatchResult:
|
|
366
|
+
"""Generate Markdown manifests for every YAML manifest in an input directory."""
|
|
367
|
+
source_dir = Path(input_dir)
|
|
368
|
+
destination = Path(output_dir)
|
|
369
|
+
results: list[AcquisitionManifestGenerationResult] = []
|
|
370
|
+
|
|
371
|
+
for manifest_path in sorted(source_dir.glob("*.yaml")):
|
|
372
|
+
payload = load_acquisition_manifest_yaml(manifest_path)
|
|
373
|
+
dataset_id = (
|
|
374
|
+
payload.get("dataset_id")
|
|
375
|
+
if isinstance(payload, dict) and isinstance(payload.get("dataset_id"), str)
|
|
376
|
+
else manifest_path.stem
|
|
377
|
+
)
|
|
378
|
+
results.append(
|
|
379
|
+
generate_acquisition_manifest_file(
|
|
380
|
+
manifest_path,
|
|
381
|
+
destination / f"{dataset_id}.md",
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
failed = [result for result in results if not result.passed]
|
|
386
|
+
if failed:
|
|
387
|
+
return AcquisitionManifestBatchResult(
|
|
388
|
+
input_dir=source_dir,
|
|
389
|
+
output_dir=destination,
|
|
390
|
+
generated=tuple(results),
|
|
391
|
+
passed=False,
|
|
392
|
+
message="; ".join(result.message for result in failed),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
396
|
+
_write_index(destination, results)
|
|
397
|
+
return AcquisitionManifestBatchResult(
|
|
398
|
+
input_dir=source_dir,
|
|
399
|
+
output_dir=destination,
|
|
400
|
+
generated=tuple(results),
|
|
401
|
+
passed=True,
|
|
402
|
+
message=f"Generated {len(results)} acquisition manifest(s).",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _write_index(
|
|
407
|
+
output_dir: Path,
|
|
408
|
+
results: list[AcquisitionManifestGenerationResult],
|
|
409
|
+
) -> None:
|
|
410
|
+
lines = [
|
|
411
|
+
"# Generated Acquisition Manifests",
|
|
412
|
+
"",
|
|
413
|
+
"These public-safe examples are generated from acquisition-manifest metadata.",
|
|
414
|
+
"",
|
|
415
|
+
"They record planning metadata only: no dataset downloaded, no acquisition executed, no redistribution rights confirmed, and no wet-lab validation by Permea.",
|
|
416
|
+
"",
|
|
417
|
+
]
|
|
418
|
+
for result in results:
|
|
419
|
+
dataset_id = result.output_path.stem
|
|
420
|
+
lines.append(f"- [{dataset_id}]({dataset_id}.md)")
|
|
421
|
+
lines.append("")
|
|
422
|
+
output_dir.joinpath("README.md").write_text("\n".join(lines), encoding="utf-8")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _entry_name(manifest: dict[str, Any]) -> str:
|
|
426
|
+
value = manifest.get("manifest_id")
|
|
427
|
+
if isinstance(value, str) and value:
|
|
428
|
+
return value
|
|
429
|
+
return "<acquisition_manifest>"
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _required_string(manifest: dict[str, Any], field: str) -> str:
|
|
433
|
+
value = manifest[field]
|
|
434
|
+
if not isinstance(value, str):
|
|
435
|
+
raise TypeError(f"{field} must be a string.")
|
|
436
|
+
return value
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _validate_enum(
|
|
440
|
+
*,
|
|
441
|
+
errors: list[AcquisitionManifestValidationError],
|
|
442
|
+
entry: str,
|
|
443
|
+
field: str,
|
|
444
|
+
value: Any,
|
|
445
|
+
allowed: frozenset[str],
|
|
446
|
+
) -> None:
|
|
447
|
+
if value in allowed:
|
|
448
|
+
return
|
|
449
|
+
errors.append(
|
|
450
|
+
AcquisitionManifestValidationError(
|
|
451
|
+
entry=entry,
|
|
452
|
+
field=field,
|
|
453
|
+
message=f"{field} must be one of: " + ", ".join(sorted(allowed)),
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _render_list(value: Any) -> str:
|
|
459
|
+
if not isinstance(value, list) or not value:
|
|
460
|
+
return "- not specified"
|
|
461
|
+
return "\n".join(f"- {item}" for item in value)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _format_validation_failure(
|
|
465
|
+
errors: tuple[AcquisitionManifestValidationError, ...],
|
|
466
|
+
) -> str:
|
|
467
|
+
details = "; ".join(
|
|
468
|
+
f"{error.entry}.{error.field}: {error.message}" for error in errors
|
|
469
|
+
)
|
|
470
|
+
return f"Acquisition manifest validation failed: {details}"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Benchmark definitions and registry surfaces for Permea Core."""
|
|
2
|
+
|
|
3
|
+
from .cards import (
|
|
4
|
+
DEFAULT_BENCHMARK_CARD_DIR,
|
|
5
|
+
BenchmarkCardBatchResult,
|
|
6
|
+
BenchmarkCardGenerationResult,
|
|
7
|
+
generate_benchmark_card_file,
|
|
8
|
+
generate_benchmark_cards_from_registry,
|
|
9
|
+
render_benchmark_card,
|
|
10
|
+
)
|
|
11
|
+
from .evidence_cards import (
|
|
12
|
+
DEFAULT_EVIDENCE_CARD_DIR,
|
|
13
|
+
DEFAULT_EVIDENCE_CARD_INPUTS,
|
|
14
|
+
EvidenceCardBatchResult,
|
|
15
|
+
EvidenceCardGenerationResult,
|
|
16
|
+
generate_evidence_card,
|
|
17
|
+
generate_evidence_cards_file,
|
|
18
|
+
generate_evidence_cards_for_inputs,
|
|
19
|
+
)
|
|
20
|
+
from .output_package import (
|
|
21
|
+
DEFAULT_OUTPUT_PACKAGE_DIR,
|
|
22
|
+
DEFAULT_OUTPUT_PACKAGE_INPUT,
|
|
23
|
+
OUTPUT_PACKAGE_FILES,
|
|
24
|
+
OutputPackageGenerationResult,
|
|
25
|
+
generate_evidence_cards,
|
|
26
|
+
generate_manifest,
|
|
27
|
+
generate_metrics,
|
|
28
|
+
generate_output_package,
|
|
29
|
+
generate_ranking,
|
|
30
|
+
)
|
|
31
|
+
from .registry import (
|
|
32
|
+
ALLOWED_MATURITY_LEVELS,
|
|
33
|
+
EXPECTED_OUTPUT_ARTIFACTS,
|
|
34
|
+
REQUIRED_REGISTRY_FIELDS,
|
|
35
|
+
BenchmarkDefinition,
|
|
36
|
+
BenchmarkRegistry,
|
|
37
|
+
RegistryValidationError,
|
|
38
|
+
RegistryValidationResult,
|
|
39
|
+
validate_registry_file,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"ALLOWED_MATURITY_LEVELS",
|
|
44
|
+
"DEFAULT_BENCHMARK_CARD_DIR",
|
|
45
|
+
"DEFAULT_EVIDENCE_CARD_DIR",
|
|
46
|
+
"DEFAULT_EVIDENCE_CARD_INPUTS",
|
|
47
|
+
"DEFAULT_OUTPUT_PACKAGE_DIR",
|
|
48
|
+
"DEFAULT_OUTPUT_PACKAGE_INPUT",
|
|
49
|
+
"EXPECTED_OUTPUT_ARTIFACTS",
|
|
50
|
+
"OUTPUT_PACKAGE_FILES",
|
|
51
|
+
"BenchmarkCardBatchResult",
|
|
52
|
+
"EvidenceCardBatchResult",
|
|
53
|
+
"EvidenceCardGenerationResult",
|
|
54
|
+
"REQUIRED_REGISTRY_FIELDS",
|
|
55
|
+
"BenchmarkCardGenerationResult",
|
|
56
|
+
"BenchmarkDefinition",
|
|
57
|
+
"BenchmarkRegistry",
|
|
58
|
+
"OutputPackageGenerationResult",
|
|
59
|
+
"RegistryValidationError",
|
|
60
|
+
"RegistryValidationResult",
|
|
61
|
+
"generate_benchmark_card_file",
|
|
62
|
+
"generate_benchmark_cards_from_registry",
|
|
63
|
+
"generate_evidence_card",
|
|
64
|
+
"generate_evidence_cards",
|
|
65
|
+
"generate_evidence_cards_file",
|
|
66
|
+
"generate_evidence_cards_for_inputs",
|
|
67
|
+
"generate_manifest",
|
|
68
|
+
"generate_metrics",
|
|
69
|
+
"generate_output_package",
|
|
70
|
+
"generate_ranking",
|
|
71
|
+
"render_benchmark_card",
|
|
72
|
+
"validate_registry_file",
|
|
73
|
+
]
|