modelwright 0.1.0a7__tar.gz → 0.1.0a8__tar.gz
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.
- {modelwright-0.1.0a7/src/modelwright.egg-info → modelwright-0.1.0a8}/PKG-INFO +2 -2
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/README.md +1 -1
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/pyproject.toml +1 -1
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/__init__.py +14 -2
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/cli.py +106 -0
- modelwright-0.1.0a8/src/modelwright/evidence.py +433 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/freshforge.py +256 -5
- {modelwright-0.1.0a7 → modelwright-0.1.0a8/src/modelwright.egg-info}/PKG-INFO +2 -2
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright.egg-info/SOURCES.txt +2 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_cli.py +150 -0
- modelwright-0.1.0a8/tests/test_evidence.py +189 -0
- modelwright-0.1.0a8/tests/test_freshforge_integration.py +586 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_import.py +1 -1
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_public_api.py +5 -0
- modelwright-0.1.0a7/tests/test_freshforge_integration.py +0 -341
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/LICENSE +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/MANIFEST.in +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/examples/README.md +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/examples/fable_2020/README.md +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/examples/fable_2020/generated_fable_2020_model.py.xz +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/examples/fable_2020/notebook_interface.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/examples/fable_2020/scenario_output_manifest.example.json +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/examples/notebooks/README.md +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/examples/notebooks/fable-2020-notebook-interface.ipynb +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/examples/notebooks/synthetic-notebook-interface.ipynb +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/examples/synthetic/notebook_interface.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/setup.cfg +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/conversion.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/evaluation.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/execution.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/extraction.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/formulas.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/formulas_oracle.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/generation.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/graph.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/notebooks.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/oracle_validation.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/oracles.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/references.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/validation.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright/wrappers.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright.egg-info/dependency_links.txt +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright.egg-info/entry_points.txt +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright.egg-info/requires.txt +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/src/modelwright.egg-info/top_level.txt +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_conversion_plan.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_dependency_graph.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_evaluation_orchestration.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_examples.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_extraction_records.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_fable_wrapper_benchmark.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_formula_expressions.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_formula_translation.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_formulas_oracle.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_generated_execution.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_generation_contract.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_materialize_fable_benchmarks.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_notebooks.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_openpyxl_extraction.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_oracle_backed_validation.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_oracle_interface.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_python_generation.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_references.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_scalar_comparison.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_supported_semantics_fixture.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_synthetic_fixture.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_validation.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_validation_regression.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_validation_report_builder.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_validation_scenario.py +0 -0
- {modelwright-0.1.0a7 → modelwright-0.1.0a8}/tests/test_wrappers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modelwright
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a8
|
|
4
4
|
Summary: Tools for converting spreadsheet workbooks into transparent Python models.
|
|
5
5
|
Author: UBC FRESH Lab
|
|
6
6
|
License-Expression: MIT
|
|
@@ -139,7 +139,7 @@ Restore the public external FABLE benchmark workbooks into ignored local paths:
|
|
|
139
139
|
scripts/bootstrap_dev_env.sh --benchmarks
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
`modelwright` is pre-release. The current alpha line is `0.1.
|
|
142
|
+
`modelwright` is pre-release. The current alpha line is `0.1.0a8`; alpha releases must not be described as full-workbook conversion guarantees.
|
|
143
143
|
|
|
144
144
|
Check release artifacts locally:
|
|
145
145
|
|
|
@@ -84,7 +84,7 @@ Restore the public external FABLE benchmark workbooks into ignored local paths:
|
|
|
84
84
|
scripts/bootstrap_dev_env.sh --benchmarks
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
`modelwright` is pre-release. The current alpha line is `0.1.
|
|
87
|
+
`modelwright` is pre-release. The current alpha line is `0.1.0a8`; alpha releases must not be described as full-workbook conversion guarantees.
|
|
88
88
|
|
|
89
89
|
Check release artifacts locally:
|
|
90
90
|
|
|
@@ -19,6 +19,13 @@ from modelwright.evaluation import (
|
|
|
19
19
|
ValidationEvaluationResult,
|
|
20
20
|
evaluate_generated_model,
|
|
21
21
|
)
|
|
22
|
+
from modelwright.evidence import (
|
|
23
|
+
ValidationEvidencePaths,
|
|
24
|
+
ValidationEvidenceSummary,
|
|
25
|
+
extract_validation_evidence,
|
|
26
|
+
validation_evidence_paths,
|
|
27
|
+
write_validation_evidence,
|
|
28
|
+
)
|
|
22
29
|
from modelwright.conversion import (
|
|
23
30
|
ConversionPlan,
|
|
24
31
|
ConversionSource,
|
|
@@ -104,7 +111,7 @@ from modelwright.wrappers import (
|
|
|
104
111
|
table,
|
|
105
112
|
)
|
|
106
113
|
|
|
107
|
-
__version__ = "0.1.
|
|
114
|
+
__version__ = "0.1.0a8"
|
|
108
115
|
|
|
109
116
|
__all__ = [
|
|
110
117
|
"CellRecord",
|
|
@@ -152,8 +159,10 @@ __all__ = [
|
|
|
152
159
|
"TableRef",
|
|
153
160
|
"TableRecord",
|
|
154
161
|
"TableView",
|
|
155
|
-
"ValidationReport",
|
|
156
162
|
"ValidationEvaluationResult",
|
|
163
|
+
"ValidationEvidencePaths",
|
|
164
|
+
"ValidationEvidenceSummary",
|
|
165
|
+
"ValidationReport",
|
|
157
166
|
"ValidationScenario",
|
|
158
167
|
"ValidationSummary",
|
|
159
168
|
"WorkbookOracle",
|
|
@@ -173,6 +182,7 @@ __all__ = [
|
|
|
173
182
|
"compare_scenarios_frame",
|
|
174
183
|
"execute_generated_model",
|
|
175
184
|
"evaluate_generated_model",
|
|
185
|
+
"extract_validation_evidence",
|
|
176
186
|
"extract_workbook",
|
|
177
187
|
"generate_python_module",
|
|
178
188
|
"infer_generated_module_contract",
|
|
@@ -187,4 +197,6 @@ __all__ = [
|
|
|
187
197
|
"table",
|
|
188
198
|
"table_frame",
|
|
189
199
|
"translate_formula_cell",
|
|
200
|
+
"validation_evidence_paths",
|
|
201
|
+
"write_validation_evidence",
|
|
190
202
|
]
|
|
@@ -12,6 +12,11 @@ from typer.main import get_command
|
|
|
12
12
|
|
|
13
13
|
from modelwright.conversion import BenchmarkRole, build_conversion_plan
|
|
14
14
|
from modelwright.evaluation import evaluate_generated_model
|
|
15
|
+
from modelwright.evidence import (
|
|
16
|
+
extract_validation_evidence,
|
|
17
|
+
validation_evidence_paths,
|
|
18
|
+
write_validation_evidence,
|
|
19
|
+
)
|
|
15
20
|
from modelwright.execution import execute_generated_model
|
|
16
21
|
from modelwright.extraction import WorkbookRecord, extract_workbook
|
|
17
22
|
from modelwright.formulas import FormulaExpression, build_formula_reference_index, translate_formula_cell
|
|
@@ -319,6 +324,78 @@ def validation_evaluate(
|
|
|
319
324
|
)
|
|
320
325
|
|
|
321
326
|
|
|
327
|
+
@validation_app.command("evidence")
|
|
328
|
+
def validation_evidence(
|
|
329
|
+
evidence_id: str = typer.Option(
|
|
330
|
+
"generated-model",
|
|
331
|
+
"--evidence-id",
|
|
332
|
+
help="Stable identifier for this compact evidence package.",
|
|
333
|
+
),
|
|
334
|
+
artifact_dir: Path | None = typer.Option(
|
|
335
|
+
None,
|
|
336
|
+
"--artifact-dir",
|
|
337
|
+
help="Directory containing generated-model workflow artifacts.",
|
|
338
|
+
),
|
|
339
|
+
output_dir: Path | None = typer.Option(
|
|
340
|
+
None,
|
|
341
|
+
"--output-dir",
|
|
342
|
+
help="Directory where compact summary.json and summary.md should be written.",
|
|
343
|
+
),
|
|
344
|
+
scenario: Path | None = typer.Option(
|
|
345
|
+
None,
|
|
346
|
+
"--scenario",
|
|
347
|
+
exists=True,
|
|
348
|
+
dir_okay=False,
|
|
349
|
+
readable=True,
|
|
350
|
+
help="Optional validation scenario JSON path. Defaults to artifact-dir/validation-scenario.json.",
|
|
351
|
+
),
|
|
352
|
+
require_artifacts: bool = typer.Option(
|
|
353
|
+
False,
|
|
354
|
+
"--require-artifacts",
|
|
355
|
+
help="Fail when any expected artifact is missing instead of writing skipped evidence.",
|
|
356
|
+
),
|
|
357
|
+
json_output: bool = typer.Option(
|
|
358
|
+
False,
|
|
359
|
+
"--json",
|
|
360
|
+
help="Emit command result as JSON.",
|
|
361
|
+
),
|
|
362
|
+
) -> None:
|
|
363
|
+
"""Package compact validation evidence from existing workflow artifacts."""
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
payload = _validation_evidence_payload(
|
|
367
|
+
evidence_id=evidence_id,
|
|
368
|
+
artifact_dir=artifact_dir,
|
|
369
|
+
output_dir=output_dir,
|
|
370
|
+
scenario=scenario,
|
|
371
|
+
require_artifacts=require_artifacts,
|
|
372
|
+
)
|
|
373
|
+
except FileNotFoundError as error:
|
|
374
|
+
error_payload: dict[str, JsonValue] = {"ok": False, "error": str(error)}
|
|
375
|
+
if json_output:
|
|
376
|
+
_emit_json(error_payload)
|
|
377
|
+
raise typer.Exit(1) from error
|
|
378
|
+
raise typer.BadParameter(str(error)) from error
|
|
379
|
+
|
|
380
|
+
if json_output:
|
|
381
|
+
_emit_json(payload)
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
summary = cast(dict[str, JsonValue], payload["summary"])
|
|
385
|
+
comparison = cast(dict[str, JsonValue], summary["comparison"])
|
|
386
|
+
typer.echo("Modelwright validation evidence")
|
|
387
|
+
typer.echo(f"Evidence status: {summary['evidence_status']}")
|
|
388
|
+
typer.echo(f"Equivalence status: {summary['equivalence_status']}")
|
|
389
|
+
typer.echo(f"Comparable outputs: {comparison.get('comparable_output_count')}")
|
|
390
|
+
typer.echo(f"Matches: {comparison.get('match_count')}")
|
|
391
|
+
typer.echo(f"Mismatches: {comparison.get('mismatch_count')}")
|
|
392
|
+
typer.echo(f"Summary JSON: {payload['summary_json_path']}")
|
|
393
|
+
typer.echo(f"Summary Markdown: {payload['summary_markdown_path']}")
|
|
394
|
+
missing = cast(list[str], summary.get("missing_artifacts", []))
|
|
395
|
+
if missing:
|
|
396
|
+
typer.echo(f"Missing artifacts: {len(missing)}")
|
|
397
|
+
|
|
398
|
+
|
|
322
399
|
@conversion_app.command("plan")
|
|
323
400
|
def conversion_plan(
|
|
324
401
|
workbook: Path = typer.Argument(..., exists=True, dir_okay=False, readable=True, help="Source workbook path."),
|
|
@@ -518,6 +595,35 @@ def _evaluate_payload(
|
|
|
518
595
|
return result.to_dict()
|
|
519
596
|
|
|
520
597
|
|
|
598
|
+
def _validation_evidence_payload(
|
|
599
|
+
*,
|
|
600
|
+
evidence_id: str,
|
|
601
|
+
artifact_dir: Path | None,
|
|
602
|
+
output_dir: Path | None,
|
|
603
|
+
scenario: Path | None,
|
|
604
|
+
require_artifacts: bool,
|
|
605
|
+
) -> dict[str, JsonValue]:
|
|
606
|
+
paths = validation_evidence_paths(
|
|
607
|
+
evidence_id=evidence_id,
|
|
608
|
+
artifact_dir=artifact_dir,
|
|
609
|
+
output_dir=output_dir,
|
|
610
|
+
validation_scenario_path=scenario,
|
|
611
|
+
)
|
|
612
|
+
summary = extract_validation_evidence(paths, require_artifacts=require_artifacts)
|
|
613
|
+
written = write_validation_evidence(summary, paths)
|
|
614
|
+
return {
|
|
615
|
+
"ok": True,
|
|
616
|
+
"evidence_id": summary.evidence_id,
|
|
617
|
+
"evidence_status": summary.evidence_status,
|
|
618
|
+
"equivalence_status": summary.equivalence_status,
|
|
619
|
+
"summary_json_path": written["summary_json_path"],
|
|
620
|
+
"summary_markdown_path": written["summary_markdown_path"],
|
|
621
|
+
"missing_artifacts": list(summary.missing_artifacts),
|
|
622
|
+
"comparison": summary.comparison,
|
|
623
|
+
"summary": summary.to_dict(),
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
|
|
521
627
|
def _conversion_plan_payload(
|
|
522
628
|
*,
|
|
523
629
|
workbook: Path,
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Compact validation-evidence summaries for generated-model workflows.
|
|
2
|
+
|
|
3
|
+
This module summarizes artifacts that were already produced by Modelwright
|
|
4
|
+
generation, execution, and validation steps. It deliberately does not rerun
|
|
5
|
+
those steps, and it does not copy raw generated source, raw generated output
|
|
6
|
+
values, workbook contents, or full comparison rows into the shareable summary.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
JsonValue = str | int | float | bool | None | list[Any] | dict[str, Any]
|
|
17
|
+
EvidenceStatus = Literal["skipped", "incomplete", "complete"]
|
|
18
|
+
EquivalenceStatus = Literal["pass", "fail", "incomplete"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ValidationEvidencePaths:
|
|
23
|
+
"""Filesystem contract for compact validation-evidence extraction."""
|
|
24
|
+
|
|
25
|
+
evidence_id: str
|
|
26
|
+
artifact_dir: Path
|
|
27
|
+
output_dir: Path
|
|
28
|
+
inference_result_path: Path
|
|
29
|
+
generation_result_path: Path
|
|
30
|
+
generated_values_path: Path
|
|
31
|
+
validation_scenario_path: Path
|
|
32
|
+
evaluation_report_path: Path
|
|
33
|
+
summary_json_path: Path
|
|
34
|
+
summary_markdown_path: Path
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def required_artifact_paths(self) -> tuple[Path, ...]:
|
|
38
|
+
"""Return artifacts used as evidence inputs."""
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
self.inference_result_path,
|
|
42
|
+
self.generation_result_path,
|
|
43
|
+
self.generated_values_path,
|
|
44
|
+
self.validation_scenario_path,
|
|
45
|
+
self.evaluation_report_path,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class ValidationEvidenceSummary:
|
|
51
|
+
"""Sanitized validation-evidence summary for publication or CI artifacts."""
|
|
52
|
+
|
|
53
|
+
evidence_id: str
|
|
54
|
+
evidence_status: EvidenceStatus
|
|
55
|
+
equivalence_status: EquivalenceStatus
|
|
56
|
+
missing_artifacts: tuple[str, ...] = field(default_factory=tuple)
|
|
57
|
+
artifacts: dict[str, str] = field(default_factory=dict)
|
|
58
|
+
stages: dict[str, JsonValue] = field(default_factory=dict)
|
|
59
|
+
comparison: dict[str, JsonValue] = field(default_factory=dict)
|
|
60
|
+
notes: tuple[str, ...] = field(default_factory=tuple)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, JsonValue]:
|
|
63
|
+
"""Serialize the compact evidence summary."""
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"evidence_id": self.evidence_id,
|
|
67
|
+
"evidence_status": self.evidence_status,
|
|
68
|
+
"equivalence_status": self.equivalence_status,
|
|
69
|
+
"missing_artifacts": list(self.missing_artifacts),
|
|
70
|
+
"artifacts": self.artifacts,
|
|
71
|
+
"stages": self.stages,
|
|
72
|
+
"comparison": self.comparison,
|
|
73
|
+
"notes": list(self.notes),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def validation_evidence_paths(
|
|
78
|
+
*,
|
|
79
|
+
evidence_id: str = "generated-model",
|
|
80
|
+
artifact_dir: str | Path | None = None,
|
|
81
|
+
output_dir: str | Path | None = None,
|
|
82
|
+
validation_scenario_path: str | Path | None = None,
|
|
83
|
+
) -> ValidationEvidencePaths:
|
|
84
|
+
"""Build the generic path contract for validation-evidence extraction."""
|
|
85
|
+
|
|
86
|
+
artifact_root = Path(artifact_dir) if artifact_dir is not None else Path("tmp/generated-model")
|
|
87
|
+
output_root = Path(output_dir) if output_dir is not None else Path("tmp/validation-evidence") / evidence_id
|
|
88
|
+
scenario_path = (
|
|
89
|
+
Path(validation_scenario_path) if validation_scenario_path is not None else artifact_root / "validation-scenario.json"
|
|
90
|
+
)
|
|
91
|
+
return ValidationEvidencePaths(
|
|
92
|
+
evidence_id=evidence_id,
|
|
93
|
+
artifact_dir=artifact_root,
|
|
94
|
+
output_dir=output_root,
|
|
95
|
+
inference_result_path=artifact_root / "inference-result.json",
|
|
96
|
+
generation_result_path=artifact_root / "generation-result.json",
|
|
97
|
+
generated_values_path=artifact_root / "generated-values.json",
|
|
98
|
+
validation_scenario_path=scenario_path,
|
|
99
|
+
evaluation_report_path=artifact_root / "evaluation-report.json",
|
|
100
|
+
summary_json_path=output_root / "summary.json",
|
|
101
|
+
summary_markdown_path=output_root / "summary.md",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def extract_validation_evidence(
|
|
106
|
+
paths: ValidationEvidencePaths | None = None,
|
|
107
|
+
*,
|
|
108
|
+
evidence_id: str = "generated-model",
|
|
109
|
+
artifact_dir: str | Path | None = None,
|
|
110
|
+
output_dir: str | Path | None = None,
|
|
111
|
+
validation_scenario_path: str | Path | None = None,
|
|
112
|
+
require_artifacts: bool = False,
|
|
113
|
+
) -> ValidationEvidenceSummary:
|
|
114
|
+
"""Extract compact evidence from existing generated-model workflow artifacts."""
|
|
115
|
+
|
|
116
|
+
evidence_paths = paths or validation_evidence_paths(
|
|
117
|
+
evidence_id=evidence_id,
|
|
118
|
+
artifact_dir=artifact_dir,
|
|
119
|
+
output_dir=output_dir,
|
|
120
|
+
validation_scenario_path=validation_scenario_path,
|
|
121
|
+
)
|
|
122
|
+
missing = tuple(str(path) for path in evidence_paths.required_artifact_paths if not path.exists())
|
|
123
|
+
if missing and require_artifacts:
|
|
124
|
+
missing_list = ", ".join(missing)
|
|
125
|
+
raise FileNotFoundError(f"missing validation evidence artifact(s): {missing_list}")
|
|
126
|
+
|
|
127
|
+
artifacts = _artifact_summary(evidence_paths)
|
|
128
|
+
if missing:
|
|
129
|
+
return ValidationEvidenceSummary(
|
|
130
|
+
evidence_id=evidence_paths.evidence_id,
|
|
131
|
+
evidence_status="skipped",
|
|
132
|
+
equivalence_status="incomplete",
|
|
133
|
+
missing_artifacts=missing,
|
|
134
|
+
artifacts=artifacts,
|
|
135
|
+
notes=("Validation artifacts are missing; evidence extraction was skipped.",),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
inference = _load_json_object(evidence_paths.inference_result_path)
|
|
139
|
+
generation = _load_json_object(evidence_paths.generation_result_path)
|
|
140
|
+
generated_values = _load_json_object(evidence_paths.generated_values_path)
|
|
141
|
+
scenario = _load_json_object(evidence_paths.validation_scenario_path)
|
|
142
|
+
evaluation = _load_json_object(evidence_paths.evaluation_report_path)
|
|
143
|
+
|
|
144
|
+
stages: dict[str, JsonValue] = {
|
|
145
|
+
"inference": _inference_stage(inference),
|
|
146
|
+
"generation": _generation_stage(generation),
|
|
147
|
+
"generated_values": _generated_values_stage(generated_values),
|
|
148
|
+
"validation_scenario": _validation_scenario_stage(scenario),
|
|
149
|
+
"evaluation": _evaluation_stage(evaluation),
|
|
150
|
+
}
|
|
151
|
+
comparison = _comparison_summary(evaluation)
|
|
152
|
+
equivalence_status = _equivalence_status(comparison)
|
|
153
|
+
evidence_status: EvidenceStatus = "complete" if equivalence_status in {"pass", "fail"} else "incomplete"
|
|
154
|
+
notes = _summary_notes(missing=(), stages=stages, comparison=comparison)
|
|
155
|
+
return ValidationEvidenceSummary(
|
|
156
|
+
evidence_id=evidence_paths.evidence_id,
|
|
157
|
+
evidence_status=evidence_status,
|
|
158
|
+
equivalence_status=equivalence_status,
|
|
159
|
+
artifacts=artifacts,
|
|
160
|
+
stages=stages,
|
|
161
|
+
comparison=comparison,
|
|
162
|
+
notes=notes,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def write_validation_evidence(
|
|
167
|
+
summary: ValidationEvidenceSummary,
|
|
168
|
+
paths: ValidationEvidencePaths | None = None,
|
|
169
|
+
*,
|
|
170
|
+
output_dir: str | Path | None = None,
|
|
171
|
+
) -> dict[str, JsonValue]:
|
|
172
|
+
"""Write ``summary.json`` and ``summary.md`` for compact evidence."""
|
|
173
|
+
|
|
174
|
+
if paths is not None:
|
|
175
|
+
json_path = paths.summary_json_path
|
|
176
|
+
markdown_path = paths.summary_markdown_path
|
|
177
|
+
else:
|
|
178
|
+
output_root = Path(output_dir) if output_dir is not None else Path("tmp/validation-evidence") / summary.evidence_id
|
|
179
|
+
json_path = output_root / "summary.json"
|
|
180
|
+
markdown_path = output_root / "summary.md"
|
|
181
|
+
|
|
182
|
+
json_path.parent.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
payload = summary.to_dict()
|
|
184
|
+
json_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
|
185
|
+
markdown_path.write_text(_summary_markdown(summary), encoding="utf-8")
|
|
186
|
+
return {
|
|
187
|
+
"summary": payload,
|
|
188
|
+
"summary_json_path": str(json_path),
|
|
189
|
+
"summary_markdown_path": str(markdown_path),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _artifact_summary(paths: ValidationEvidencePaths) -> dict[str, str]:
|
|
194
|
+
return {
|
|
195
|
+
"artifact_dir": str(paths.artifact_dir),
|
|
196
|
+
"inference_result": str(paths.inference_result_path),
|
|
197
|
+
"generation_result": str(paths.generation_result_path),
|
|
198
|
+
"generated_values": str(paths.generated_values_path),
|
|
199
|
+
"validation_scenario": str(paths.validation_scenario_path),
|
|
200
|
+
"evaluation_report": str(paths.evaluation_report_path),
|
|
201
|
+
"summary_json": str(paths.summary_json_path),
|
|
202
|
+
"summary_markdown": str(paths.summary_markdown_path),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _inference_stage(data: dict[str, JsonValue]) -> dict[str, JsonValue]:
|
|
207
|
+
contract = _object(data.get("contract"))
|
|
208
|
+
return {
|
|
209
|
+
"available": True,
|
|
210
|
+
"inferred": bool(data.get("inferred", False)),
|
|
211
|
+
"input_ref_count": _length(contract.get("input_refs")),
|
|
212
|
+
"output_ref_count": _length(contract.get("output_refs")),
|
|
213
|
+
"symbol_count": _length(contract.get("symbols")),
|
|
214
|
+
"expression_count": _length(data.get("expressions")),
|
|
215
|
+
"constant_count": _length(data.get("constants")),
|
|
216
|
+
"diagnostic_count": _diagnostic_count(data),
|
|
217
|
+
"error_diagnostic_count": _diagnostic_count(data, severity="error"),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _generation_stage(data: dict[str, JsonValue]) -> dict[str, JsonValue]:
|
|
222
|
+
contract = _object(data.get("contract"))
|
|
223
|
+
source_code = data.get("source_code")
|
|
224
|
+
source_size = len(source_code.encode("utf-8")) if isinstance(source_code, str) else None
|
|
225
|
+
source_lines = source_code.count("\n") + 1 if isinstance(source_code, str) and source_code else None
|
|
226
|
+
return {
|
|
227
|
+
"available": True,
|
|
228
|
+
"generated": bool(data.get("generated", False)),
|
|
229
|
+
"input_ref_count": _length(contract.get("input_refs")),
|
|
230
|
+
"output_ref_count": _length(contract.get("output_refs")),
|
|
231
|
+
"symbol_count": _length(contract.get("symbols")),
|
|
232
|
+
"source_size_bytes": source_size,
|
|
233
|
+
"source_line_count": source_lines,
|
|
234
|
+
"diagnostic_count": _diagnostic_count(data),
|
|
235
|
+
"error_diagnostic_count": _diagnostic_count(data, severity="error"),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _generated_values_stage(data: dict[str, JsonValue]) -> dict[str, JsonValue]:
|
|
240
|
+
contract = _object(data.get("contract"))
|
|
241
|
+
output_values = _object(data.get("output_values"))
|
|
242
|
+
return {
|
|
243
|
+
"available": True,
|
|
244
|
+
"executed": bool(data.get("executed", False)),
|
|
245
|
+
"declared_output_count": _length(contract.get("output_refs")),
|
|
246
|
+
"generated_output_count": len(output_values),
|
|
247
|
+
"diagnostic_count": _diagnostic_count(data),
|
|
248
|
+
"error_diagnostic_count": _diagnostic_count(data, severity="error"),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _validation_scenario_stage(data: dict[str, JsonValue]) -> dict[str, JsonValue]:
|
|
253
|
+
outputs = _sequence(data.get("outputs"))
|
|
254
|
+
output_kinds = sorted(
|
|
255
|
+
{item.get("kind") for item in outputs if isinstance(item, dict) and isinstance(item.get("kind"), str)}
|
|
256
|
+
)
|
|
257
|
+
return {
|
|
258
|
+
"available": True,
|
|
259
|
+
"scenario_id": data.get("scenario_id"),
|
|
260
|
+
"input_count": _length(data.get("inputs")),
|
|
261
|
+
"output_count": len(outputs),
|
|
262
|
+
"output_kinds": output_kinds,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _evaluation_stage(data: dict[str, JsonValue]) -> dict[str, JsonValue]:
|
|
267
|
+
generated_execution = _object(data.get("generated_execution"))
|
|
268
|
+
cached_report = _object(data.get("cached_validation_report"))
|
|
269
|
+
oracle_report = _object(data.get("oracle_validation_report"))
|
|
270
|
+
return {
|
|
271
|
+
"available": True,
|
|
272
|
+
"scenario_id": data.get("scenario_id"),
|
|
273
|
+
"generated_executed": bool(generated_execution.get("executed", False)),
|
|
274
|
+
"generated_output_count": _length(generated_execution.get("output_values")),
|
|
275
|
+
"has_cached_validation_report": bool(cached_report),
|
|
276
|
+
"has_oracle_validation_report": bool(oracle_report),
|
|
277
|
+
"cached_validation_status": cached_report.get("status"),
|
|
278
|
+
"oracle_validation_status": oracle_report.get("status"),
|
|
279
|
+
"diagnostic_count": _diagnostic_count(data),
|
|
280
|
+
"error_diagnostic_count": _diagnostic_count(data, severity="error"),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _comparison_summary(evaluation: dict[str, JsonValue]) -> dict[str, JsonValue]:
|
|
285
|
+
reports = [
|
|
286
|
+
_object(evaluation.get("cached_validation_report")),
|
|
287
|
+
_object(evaluation.get("oracle_validation_report")),
|
|
288
|
+
]
|
|
289
|
+
report = next((item for item in reports if item), None)
|
|
290
|
+
if report is None:
|
|
291
|
+
return {
|
|
292
|
+
"comparison_counts_available": False,
|
|
293
|
+
"comparable_output_count": None,
|
|
294
|
+
"match_count": None,
|
|
295
|
+
"mismatch_count": None,
|
|
296
|
+
"validation_report_status": None,
|
|
297
|
+
"validation_backend": None,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
comparisons = _sequence(report.get("comparisons"))
|
|
301
|
+
mismatches = _sequence(report.get("mismatches"))
|
|
302
|
+
comparable_count = _find_count(report, _COMPARABLE_KEYS)
|
|
303
|
+
match_count = _find_count(report, _MATCH_KEYS)
|
|
304
|
+
mismatch_count = _find_count(report, _MISMATCH_KEYS)
|
|
305
|
+
if comparable_count is None and comparisons:
|
|
306
|
+
comparable_count = len(comparisons)
|
|
307
|
+
if match_count is None and comparisons:
|
|
308
|
+
match_count = sum(1 for item in comparisons if isinstance(item, dict) and item.get("matches") is True)
|
|
309
|
+
if mismatch_count is None and (comparisons or mismatches):
|
|
310
|
+
mismatch_count = len(mismatches) if mismatches else sum(
|
|
311
|
+
1 for item in comparisons if isinstance(item, dict) and item.get("matches") is False
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
counts_available = comparable_count is not None and match_count is not None and mismatch_count is not None
|
|
315
|
+
return {
|
|
316
|
+
"comparison_counts_available": counts_available,
|
|
317
|
+
"comparable_output_count": comparable_count,
|
|
318
|
+
"match_count": match_count,
|
|
319
|
+
"mismatch_count": mismatch_count,
|
|
320
|
+
"validation_report_status": report.get("status"),
|
|
321
|
+
"validation_backend": report.get("oracle_backend"),
|
|
322
|
+
"validation_diagnostic_count": _diagnostic_count(report),
|
|
323
|
+
"validation_error_diagnostic_count": _diagnostic_count(report, severity="error"),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _equivalence_status(comparison: dict[str, JsonValue]) -> EquivalenceStatus:
|
|
328
|
+
comparable = comparison.get("comparable_output_count")
|
|
329
|
+
matches = comparison.get("match_count")
|
|
330
|
+
mismatches = comparison.get("mismatch_count")
|
|
331
|
+
if not all(isinstance(value, int) and not isinstance(value, bool) for value in (comparable, matches, mismatches)):
|
|
332
|
+
return "incomplete"
|
|
333
|
+
if comparable == matches and mismatches == 0:
|
|
334
|
+
return "pass"
|
|
335
|
+
return "fail"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _summary_notes(
|
|
339
|
+
*,
|
|
340
|
+
missing: tuple[str, ...],
|
|
341
|
+
stages: dict[str, JsonValue],
|
|
342
|
+
comparison: dict[str, JsonValue],
|
|
343
|
+
) -> tuple[str, ...]:
|
|
344
|
+
notes: list[str] = []
|
|
345
|
+
if missing:
|
|
346
|
+
notes.append("Validation artifacts are missing; evidence extraction was skipped.")
|
|
347
|
+
if comparison.get("comparison_counts_available") is not True:
|
|
348
|
+
notes.append("Explicit comparable-output, match, and mismatch counts were not found.")
|
|
349
|
+
diagnostics = sum(
|
|
350
|
+
int(stage.get("diagnostic_count", 0))
|
|
351
|
+
for stage in stages.values()
|
|
352
|
+
if isinstance(stage, dict) and isinstance(stage.get("diagnostic_count"), int)
|
|
353
|
+
)
|
|
354
|
+
if diagnostics:
|
|
355
|
+
notes.append(f"Stage diagnostics were reported: {diagnostics}.")
|
|
356
|
+
return tuple(notes)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _summary_markdown(summary: ValidationEvidenceSummary) -> str:
|
|
360
|
+
comparison = summary.comparison
|
|
361
|
+
lines = [
|
|
362
|
+
f"# Modelwright Validation Evidence: {summary.evidence_id}",
|
|
363
|
+
"",
|
|
364
|
+
f"- Evidence status: `{summary.evidence_status}`",
|
|
365
|
+
f"- Equivalence status: `{summary.equivalence_status}`",
|
|
366
|
+
f"- Comparable outputs: `{comparison.get('comparable_output_count')}`",
|
|
367
|
+
f"- Matches: `{comparison.get('match_count')}`",
|
|
368
|
+
f"- Mismatches: `{comparison.get('mismatch_count')}`",
|
|
369
|
+
"",
|
|
370
|
+
"## Artifacts",
|
|
371
|
+
"",
|
|
372
|
+
]
|
|
373
|
+
for key, path in summary.artifacts.items():
|
|
374
|
+
lines.append(f"- `{key}`: `{path}`")
|
|
375
|
+
if summary.missing_artifacts:
|
|
376
|
+
lines.extend(["", "## Missing Artifacts", ""])
|
|
377
|
+
lines.extend(f"- `{path}`" for path in summary.missing_artifacts)
|
|
378
|
+
if summary.notes:
|
|
379
|
+
lines.extend(["", "## Notes", ""])
|
|
380
|
+
lines.extend(f"- {note}" for note in summary.notes)
|
|
381
|
+
lines.append("")
|
|
382
|
+
return "\n".join(lines)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _load_json_object(path: Path) -> dict[str, JsonValue]:
|
|
386
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
387
|
+
if not isinstance(data, dict):
|
|
388
|
+
raise ValueError(f"expected JSON object in {path}")
|
|
389
|
+
return data
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _object(value: JsonValue) -> dict[str, JsonValue]:
|
|
393
|
+
return value if isinstance(value, dict) else {}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _sequence(value: JsonValue) -> list[JsonValue]:
|
|
397
|
+
return value if isinstance(value, list) else []
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _length(value: JsonValue) -> int | None:
|
|
401
|
+
if isinstance(value, dict | list):
|
|
402
|
+
return len(value)
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _diagnostic_count(data: dict[str, JsonValue], *, severity: str | None = None) -> int:
|
|
407
|
+
diagnostics = _sequence(data.get("diagnostics"))
|
|
408
|
+
if severity is None:
|
|
409
|
+
return len(diagnostics)
|
|
410
|
+
return sum(1 for item in diagnostics if isinstance(item, dict) and item.get("severity") == severity)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
_COMPARABLE_KEYS = ("comparable_output_count", "comparable_outputs", "total_comparable_outputs")
|
|
414
|
+
_MATCH_KEYS = ("match_count", "matches", "matched_outputs", "generated_output_matches")
|
|
415
|
+
_MISMATCH_KEYS = ("mismatch_count", "mismatches", "mismatched_outputs")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _find_count(data: JsonValue, keys: tuple[str, ...]) -> int | None:
|
|
419
|
+
if isinstance(data, dict):
|
|
420
|
+
for key in keys:
|
|
421
|
+
value = data.get(key)
|
|
422
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
423
|
+
return value
|
|
424
|
+
for value in data.values():
|
|
425
|
+
found = _find_count(value, keys)
|
|
426
|
+
if found is not None:
|
|
427
|
+
return found
|
|
428
|
+
elif isinstance(data, list):
|
|
429
|
+
for item in data:
|
|
430
|
+
found = _find_count(item, keys)
|
|
431
|
+
if found is not None:
|
|
432
|
+
return found
|
|
433
|
+
return None
|