fresh-vdyp 0.1.0__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.
Files changed (63) hide show
  1. fresh_vdyp-0.1.0/LICENSE +21 -0
  2. fresh_vdyp-0.1.0/PKG-INFO +126 -0
  3. fresh_vdyp-0.1.0/README.md +110 -0
  4. fresh_vdyp-0.1.0/pyproject.toml +92 -0
  5. fresh_vdyp-0.1.0/scripts/assistant_tools.py +262 -0
  6. fresh_vdyp-0.1.0/scripts/bundle_service.py +236 -0
  7. fresh_vdyp-0.1.0/scripts/check_workflow.py +1194 -0
  8. fresh_vdyp-0.1.0/scripts/coe_parser.py +600 -0
  9. fresh_vdyp-0.1.0/scripts/config_parser.py +566 -0
  10. fresh_vdyp-0.1.0/scripts/curve_api.py +566 -0
  11. fresh_vdyp-0.1.0/scripts/dashboard.py +313 -0
  12. fresh_vdyp-0.1.0/scripts/equations.py +1051 -0
  13. fresh_vdyp-0.1.0/scripts/fresh_vdyp/__init__.py +58 -0
  14. fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/PKG-INFO +126 -0
  15. fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/SOURCES.txt +61 -0
  16. fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/dependency_links.txt +1 -0
  17. fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/entry_points.txt +3 -0
  18. fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/requires.txt +5 -0
  19. fresh_vdyp-0.1.0/scripts/fresh_vdyp.egg-info/top_level.txt +20 -0
  20. fresh_vdyp-0.1.0/scripts/fvdyp_api.py +317 -0
  21. fresh_vdyp-0.1.0/scripts/fvdyp_cli.py +259 -0
  22. fresh_vdyp-0.1.0/scripts/inventory_vdyp.py +381 -0
  23. fresh_vdyp-0.1.0/scripts/key_inference.py +492 -0
  24. fresh_vdyp-0.1.0/scripts/normalize.py +459 -0
  25. fresh_vdyp-0.1.0/scripts/provenance.py +383 -0
  26. fresh_vdyp-0.1.0/scripts/run_console.py +492 -0
  27. fresh_vdyp-0.1.0/scripts/vdyp_control.py +500 -0
  28. fresh_vdyp-0.1.0/scripts/vdyp_map.py +349 -0
  29. fresh_vdyp-0.1.0/scripts/vdyp_runtime.py +355 -0
  30. fresh_vdyp-0.1.0/scripts/yield_parser.py +445 -0
  31. fresh_vdyp-0.1.0/setup.cfg +4 -0
  32. fresh_vdyp-0.1.0/tests/test_api_docstrings.py +65 -0
  33. fresh_vdyp-0.1.0/tests/test_assistant_tools.py +96 -0
  34. fresh_vdyp-0.1.0/tests/test_bundle_service.py +202 -0
  35. fresh_vdyp-0.1.0/tests/test_check_workflow.py +809 -0
  36. fresh_vdyp-0.1.0/tests/test_cli.py +75 -0
  37. fresh_vdyp-0.1.0/tests/test_coe_parser.py +220 -0
  38. fresh_vdyp-0.1.0/tests/test_coe_schemas.py +41 -0
  39. fresh_vdyp-0.1.0/tests/test_config_parser.py +157 -0
  40. fresh_vdyp-0.1.0/tests/test_console_fixture.py +66 -0
  41. fresh_vdyp-0.1.0/tests/test_curve_api.py +180 -0
  42. fresh_vdyp-0.1.0/tests/test_curve_properties.py +54 -0
  43. fresh_vdyp-0.1.0/tests/test_dashboard.py +217 -0
  44. fresh_vdyp-0.1.0/tests/test_end_to_end.py +67 -0
  45. fresh_vdyp-0.1.0/tests/test_equation_matrix.py +56 -0
  46. fresh_vdyp-0.1.0/tests/test_equations.py +674 -0
  47. fresh_vdyp-0.1.0/tests/test_fixtures.py +63 -0
  48. fresh_vdyp-0.1.0/tests/test_fvdyp_api.py +125 -0
  49. fresh_vdyp-0.1.0/tests/test_fvdyp_cli.py +148 -0
  50. fresh_vdyp-0.1.0/tests/test_inventory_vdyp.py +227 -0
  51. fresh_vdyp-0.1.0/tests/test_key_inference.py +206 -0
  52. fresh_vdyp-0.1.0/tests/test_normalize.py +224 -0
  53. fresh_vdyp-0.1.0/tests/test_package_metadata.py +34 -0
  54. fresh_vdyp-0.1.0/tests/test_parser_properties.py +38 -0
  55. fresh_vdyp-0.1.0/tests/test_properties.py +110 -0
  56. fresh_vdyp-0.1.0/tests/test_provenance.py +170 -0
  57. fresh_vdyp-0.1.0/tests/test_release_regression.py +63 -0
  58. fresh_vdyp-0.1.0/tests/test_run_console.py +265 -0
  59. fresh_vdyp-0.1.0/tests/test_vdyp_control.py +215 -0
  60. fresh_vdyp-0.1.0/tests/test_vdyp_map.py +223 -0
  61. fresh_vdyp-0.1.0/tests/test_vdyp_runtime.py +213 -0
  62. fresh_vdyp-0.1.0/tests/test_workflow.py +19 -0
  63. fresh_vdyp-0.1.0/tests/test_yield_parser.py +247 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UBC FRESH Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: fresh-vdyp
3
+ Version: 0.1.0
4
+ Summary: A public interoperability layer for the BC VDYP7 coefficient bundle, console runner, and yield curves.
5
+ Author: UBC FRESH Lab
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: fastapi>=0.110
11
+ Requires-Dist: httpx>=0.27
12
+ Requires-Dist: pydantic>=2
13
+ Requires-Dist: typer>=0.12
14
+ Requires-Dist: uvicorn>=0.29
15
+ Dynamic: license-file
16
+
17
+ # fresh-vdyp
18
+
19
+ A public, tested Python package for reading the BC VDYP7 coefficient and control
20
+ bundle, wrapping the VDYP7Console runner, exposing typed yield curves, and
21
+ validating incremental Python growth equations against the official console
22
+ outputs.
23
+
24
+ `fresh-vdyp` is an interoperability and reimplementation layer. It does not
25
+ claim authorship of VDYP7 or BC government data. It exists so the model's
26
+ plain-text coefficients, control files, and yield tables can be catalogued,
27
+ queried, tested, and used by tools such as FEMIC without a GUI boundary.
28
+
29
+ Repository: <https://github.com/UBC-FRESH/fresh-vdyp>
30
+
31
+ ## Status
32
+
33
+ All phases P0-P7 are complete. `ROADMAP.md` lists the phases,
34
+ and GitHub issues carry the live status. The first working milestone is the
35
+ source inventory and coefficient catalog, not a full VDYP growth-model rewrite.
36
+
37
+ ## What this is
38
+
39
+ - A parser and catalog for the VDYP7 `VDYP_CFG/coe` bundle.
40
+ - A control-file map for `VDYP.CTR`, `VDYPBACK.CTR`, `.COE`, `.PRM`, and `.DAT`.
41
+ - A runner and parser for `VDYP7Console` CSV yield tables.
42
+ - A typed Python curve API and public data assets.
43
+ - An incremental Python equation layer validated one family at a time.
44
+ - A Typer CLI, FastAPI service, FRESH-style web UI, and bounded LLM assistant.
45
+ - A package that FEMIC and other UBC-FRESH tools can depend on.
46
+
47
+ ## What this is not
48
+
49
+ - A claim that UBC-FRESH invented VDYP7.
50
+ - A byte-for-byte clone of the native VDYP7 binaries.
51
+ - A substitute for measured field data or a complete growth-model validation.
52
+ - A place to commit raw government model bundles or credentials.
53
+
54
+ ## Source and attribution
55
+
56
+ Every extracted coefficient, control file, curve, and derived value records:
57
+
58
+ ```text
59
+ source
60
+ source version
61
+ source file
62
+ source SHA256
63
+ extraction date
64
+ citation
65
+ ```
66
+
67
+ The original source remains the BC government's work. Reimplemented logic is
68
+ labelled `reimplemented` and validated against console outputs where possible.
69
+
70
+ ## Plan
71
+
72
+ Read these in order:
73
+
74
+ 1. [`notes/plan/0001-program-plan.md`](notes/plan/0001-program-plan.md)
75
+ 2. [`notes/plan/0002-data-provenance.md`](notes/plan/0002-data-provenance.md)
76
+ 3. [`notes/plan/0003-architecture.md`](notes/plan/0003-architecture.md)
77
+ 4. [`notes/plan/0004-validation-release.md`](notes/plan/0004-validation-release.md)
78
+ 5. [`notes/reference/vdyp-inventory.md`](notes/reference/vdyp-inventory.md)
79
+ 6. [`notes/reference/vdyp-model-map.md`](notes/reference/vdyp-model-map.md)
80
+
81
+ ## Quick start
82
+
83
+ This seed is a planning repository. The Python package and data workflows land
84
+ in Phase 2. Until then, the workflow gates are:
85
+
86
+ ```bash
87
+ python -m venv .venv
88
+ . .venv/bin/activate
89
+ python -m pip install -r requirements-dev.txt
90
+ python scripts/dashboard.py
91
+ ```
92
+
93
+ ## Bundle inspection
94
+
95
+ Until the installable package lands, the first commands run from the repository:
96
+
97
+ ```bash
98
+ export VDYP_CFG_DIR=/path/to/VDYP_CFG
99
+ python scripts/fvdyp_cli.py bundle validate
100
+ python scripts/fvdyp_cli.py bundle catalog
101
+ python scripts/fvdyp_cli.py bundle extract --output-dir data/normalized
102
+ ```
103
+
104
+ `validate` checks required files, control targets, and key lookups. `catalog`
105
+ prints the family and row counts. `extract` writes normalized CSV files plus a
106
+ provenance manifest; the CSV files stay under ignored `data/normalized/`.
107
+
108
+ ## Repository map
109
+
110
+ | Path | Purpose |
111
+ | --- | --- |
112
+ | `AGENTS.md` | Operating contract for coding agents |
113
+ | `HANDOFF.md` | First-read handoff note for a new session agent |
114
+ | `ROADMAP.md` | Plan of record and issue tracker map |
115
+ | `CHANGE_LOG.md` | Dated narrative of changes |
116
+ | `notes/plan/` | Detailed program, data, architecture, and validation plans |
117
+ | `notes/reference/` | VDYP inventory and coefficient-map facts |
118
+ | `notes/adr/` | Accepted design decisions |
119
+ | `docs/` | Sphinx RTFM surface |
120
+ | `scripts/check_workflow.py` | Workflow validator |
121
+ | `tests/` | pytest suite, including planning-surface checks |
122
+
123
+ ## License and citation
124
+
125
+ Code is MIT licensed. Coefficient files and model outputs retain their original
126
+ BC government attribution. Cite the source model and file, not only this package.
@@ -0,0 +1,110 @@
1
+ # fresh-vdyp
2
+
3
+ A public, tested Python package for reading the BC VDYP7 coefficient and control
4
+ bundle, wrapping the VDYP7Console runner, exposing typed yield curves, and
5
+ validating incremental Python growth equations against the official console
6
+ outputs.
7
+
8
+ `fresh-vdyp` is an interoperability and reimplementation layer. It does not
9
+ claim authorship of VDYP7 or BC government data. It exists so the model's
10
+ plain-text coefficients, control files, and yield tables can be catalogued,
11
+ queried, tested, and used by tools such as FEMIC without a GUI boundary.
12
+
13
+ Repository: <https://github.com/UBC-FRESH/fresh-vdyp>
14
+
15
+ ## Status
16
+
17
+ All phases P0-P7 are complete. `ROADMAP.md` lists the phases,
18
+ and GitHub issues carry the live status. The first working milestone is the
19
+ source inventory and coefficient catalog, not a full VDYP growth-model rewrite.
20
+
21
+ ## What this is
22
+
23
+ - A parser and catalog for the VDYP7 `VDYP_CFG/coe` bundle.
24
+ - A control-file map for `VDYP.CTR`, `VDYPBACK.CTR`, `.COE`, `.PRM`, and `.DAT`.
25
+ - A runner and parser for `VDYP7Console` CSV yield tables.
26
+ - A typed Python curve API and public data assets.
27
+ - An incremental Python equation layer validated one family at a time.
28
+ - A Typer CLI, FastAPI service, FRESH-style web UI, and bounded LLM assistant.
29
+ - A package that FEMIC and other UBC-FRESH tools can depend on.
30
+
31
+ ## What this is not
32
+
33
+ - A claim that UBC-FRESH invented VDYP7.
34
+ - A byte-for-byte clone of the native VDYP7 binaries.
35
+ - A substitute for measured field data or a complete growth-model validation.
36
+ - A place to commit raw government model bundles or credentials.
37
+
38
+ ## Source and attribution
39
+
40
+ Every extracted coefficient, control file, curve, and derived value records:
41
+
42
+ ```text
43
+ source
44
+ source version
45
+ source file
46
+ source SHA256
47
+ extraction date
48
+ citation
49
+ ```
50
+
51
+ The original source remains the BC government's work. Reimplemented logic is
52
+ labelled `reimplemented` and validated against console outputs where possible.
53
+
54
+ ## Plan
55
+
56
+ Read these in order:
57
+
58
+ 1. [`notes/plan/0001-program-plan.md`](notes/plan/0001-program-plan.md)
59
+ 2. [`notes/plan/0002-data-provenance.md`](notes/plan/0002-data-provenance.md)
60
+ 3. [`notes/plan/0003-architecture.md`](notes/plan/0003-architecture.md)
61
+ 4. [`notes/plan/0004-validation-release.md`](notes/plan/0004-validation-release.md)
62
+ 5. [`notes/reference/vdyp-inventory.md`](notes/reference/vdyp-inventory.md)
63
+ 6. [`notes/reference/vdyp-model-map.md`](notes/reference/vdyp-model-map.md)
64
+
65
+ ## Quick start
66
+
67
+ This seed is a planning repository. The Python package and data workflows land
68
+ in Phase 2. Until then, the workflow gates are:
69
+
70
+ ```bash
71
+ python -m venv .venv
72
+ . .venv/bin/activate
73
+ python -m pip install -r requirements-dev.txt
74
+ python scripts/dashboard.py
75
+ ```
76
+
77
+ ## Bundle inspection
78
+
79
+ Until the installable package lands, the first commands run from the repository:
80
+
81
+ ```bash
82
+ export VDYP_CFG_DIR=/path/to/VDYP_CFG
83
+ python scripts/fvdyp_cli.py bundle validate
84
+ python scripts/fvdyp_cli.py bundle catalog
85
+ python scripts/fvdyp_cli.py bundle extract --output-dir data/normalized
86
+ ```
87
+
88
+ `validate` checks required files, control targets, and key lookups. `catalog`
89
+ prints the family and row counts. `extract` writes normalized CSV files plus a
90
+ provenance manifest; the CSV files stay under ignored `data/normalized/`.
91
+
92
+ ## Repository map
93
+
94
+ | Path | Purpose |
95
+ | --- | --- |
96
+ | `AGENTS.md` | Operating contract for coding agents |
97
+ | `HANDOFF.md` | First-read handoff note for a new session agent |
98
+ | `ROADMAP.md` | Plan of record and issue tracker map |
99
+ | `CHANGE_LOG.md` | Dated narrative of changes |
100
+ | `notes/plan/` | Detailed program, data, architecture, and validation plans |
101
+ | `notes/reference/` | VDYP inventory and coefficient-map facts |
102
+ | `notes/adr/` | Accepted design decisions |
103
+ | `docs/` | Sphinx RTFM surface |
104
+ | `scripts/check_workflow.py` | Workflow validator |
105
+ | `tests/` | pytest suite, including planning-surface checks |
106
+
107
+ ## License and citation
108
+
109
+ Code is MIT licensed. Coefficient files and model outputs retain their original
110
+ BC government attribution. Cite the source model and file, not only this package.
@@ -0,0 +1,92 @@
1
+ # Build and tool configuration for fresh-vdyp. The installable modules live in
2
+ # scripts/ and the public facade lives in scripts/fresh_vdyp/. Each gate
3
+ # configured here is also a check in the green dashboard (scripts/dashboard.py).
4
+
5
+ [build-system]
6
+ requires = ["setuptools>=69", "wheel"]
7
+ build-backend = "setuptools.build_meta"
8
+
9
+ [project]
10
+ name = "fresh-vdyp"
11
+ version = "0.1.0"
12
+ description = "A public interoperability layer for the BC VDYP7 coefficient bundle, console runner, and yield curves."
13
+ readme = "README.md"
14
+ requires-python = ">=3.11"
15
+ license = "MIT"
16
+ authors = [{ name = "UBC FRESH Lab" }]
17
+ dependencies = [
18
+ "fastapi>=0.110",
19
+ "httpx>=0.27",
20
+ "pydantic>=2",
21
+ "typer>=0.12",
22
+ "uvicorn>=0.29",
23
+ ]
24
+
25
+ [project.scripts]
26
+ fvdyp = "fvdyp_cli:main"
27
+ fresh-vdyp = "fvdyp_cli:main"
28
+
29
+ [tool.setuptools]
30
+ package-dir = { "" = "scripts" }
31
+ packages = ["fresh_vdyp"]
32
+ py-modules = [
33
+ "assistant_tools",
34
+ "bundle_service",
35
+ "check_workflow",
36
+ "coe_parser",
37
+ "config_parser",
38
+ "curve_api",
39
+ "dashboard",
40
+ "equations",
41
+ "fvdyp_api",
42
+ "fvdyp_cli",
43
+ "inventory_vdyp",
44
+ "key_inference",
45
+ "normalize",
46
+ "provenance",
47
+ "run_console",
48
+ "vdyp_control",
49
+ "vdyp_map",
50
+ "vdyp_runtime",
51
+ "yield_parser",
52
+ ]
53
+
54
+ [tool.ruff]
55
+ line-length = 100
56
+ target-version = "py311"
57
+ src = ["scripts", "tests"]
58
+ extend-exclude = ["tmp", "_build"]
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "W", "F", "I", "B", "UP", "D", "ANN", "N", "PT", "SIM", "RUF", "C4"]
62
+
63
+ [tool.ruff.lint.pydocstyle]
64
+ convention = "numpy"
65
+
66
+ [tool.ruff.lint.per-file-ignores]
67
+ "tests/**" = ["D"]
68
+ "templates/project/tests/**" = ["D"]
69
+ "docs/conf.py" = ["D", "ANN", "N"]
70
+
71
+ [tool.mypy]
72
+ python_version = "3.11"
73
+ strict = true
74
+ warn_unreachable = true
75
+ mypy_path = "scripts"
76
+ files = ["scripts", "tests", "docs/conf.py"]
77
+
78
+ [[tool.mypy.overrides]]
79
+ module = ["numpydoc", "numpydoc.*"]
80
+ ignore_missing_imports = true
81
+
82
+ [tool.pytest.ini_options]
83
+ testpaths = ["tests"]
84
+ addopts = "--cov=scripts --cov-branch --cov-report=term-missing --cov-fail-under=95"
85
+
86
+ [tool.coverage.run]
87
+ branch = true
88
+ source = ["scripts"]
89
+
90
+ [tool.coverage.report]
91
+ show_missing = true
92
+ exclude_also = ['if __name__ == "__main__":']
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env python3
2
+ """Bounded assistant tools over the catalog, curves, provenance, and matrix.
3
+
4
+ Every handler is deterministic and returns an :class:`AssistantAnswer` with at
5
+ least one source. The validator rejects an answer without a source, so an
6
+ assistant cannot present a coefficient, equation, or curve without attribution.
7
+ No network or model provider is used.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+ from typing import Final
16
+
17
+ from bundle_service import catalog_payload
18
+ from curve_api import CurveQuery, CurveService
19
+ from yield_parser import DEFAULT_ALIASES, DEFAULT_SCHEMA
20
+
21
+ DEFAULT_MATRIX: Final[Path] = Path("notes/reference/vdyp-equation-matrix.json")
22
+
23
+
24
+ class AssistantAnswerError(ValueError):
25
+ """Raised when an answer has no usable source."""
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ToolSpec:
30
+ """The schema of one assistant tool.
31
+
32
+ Attributes
33
+ ----------
34
+ name : str
35
+ Tool name.
36
+ description : str
37
+ What the tool answers.
38
+ parameters : dict of str to str
39
+ Parameter names and their descriptions.
40
+ """
41
+
42
+ name: str
43
+ description: str
44
+ parameters: dict[str, str] = field(default_factory=dict)
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class AssistantAnswer:
49
+ """One source-backed assistant answer.
50
+
51
+ Attributes
52
+ ----------
53
+ text : str
54
+ Short answer text.
55
+ sources : tuple of str
56
+ Source labels. Must be non-empty.
57
+ data : dict of str to object
58
+ Structured result for a caller.
59
+ """
60
+
61
+ text: str
62
+ sources: tuple[str, ...]
63
+ data: dict[str, object] = field(default_factory=dict)
64
+
65
+ def to_dict(self) -> dict[str, object]:
66
+ """Render the answer as a JSON-ready mapping.
67
+
68
+ Returns
69
+ -------
70
+ dict of str to object
71
+ Text, sources, and data.
72
+ """
73
+ return {"text": self.text, "sources": list(self.sources), "data": self.data}
74
+
75
+
76
+ TOOL_SPECS: Final[tuple[ToolSpec, ...]] = (
77
+ ToolSpec(
78
+ "catalog_search",
79
+ "Search the coefficient catalog by family, equation family, or status.",
80
+ {"query": "case-insensitive text"},
81
+ ),
82
+ ToolSpec(
83
+ "curve_query",
84
+ "Query the typed curve service with optional species/year/age filters.",
85
+ {
86
+ "species": "species code",
87
+ "year_start": "int",
88
+ "year_end": "int",
89
+ "age_start": "int",
90
+ "age_end": "int",
91
+ },
92
+ ),
93
+ ToolSpec(
94
+ "provenance_lookup",
95
+ "Return the source and runtime provenance of the loaded curve table.",
96
+ {},
97
+ ),
98
+ ToolSpec(
99
+ "validation_status",
100
+ "Return the validation status of one equation family.",
101
+ {"family": "family name"},
102
+ ),
103
+ )
104
+
105
+
106
+ def validate_answer(answer: AssistantAnswer) -> None:
107
+ """Validate that an answer names at least one source.
108
+
109
+ Parameters
110
+ ----------
111
+ answer : AssistantAnswer
112
+ Answer to validate.
113
+
114
+ Returns
115
+ -------
116
+ None
117
+ The answer is source-backed.
118
+
119
+ Raises
120
+ ------
121
+ AssistantAnswerError
122
+ When the source tuple is empty or contains a blank label.
123
+ """
124
+ if not answer.sources:
125
+ raise AssistantAnswerError("answer has no source")
126
+ if any(not source.strip() for source in answer.sources):
127
+ raise AssistantAnswerError("answer has a blank source")
128
+
129
+
130
+ def catalog_search(config_dir: Path, query: str) -> AssistantAnswer:
131
+ """Search the catalog and return matching families with their source files.
132
+
133
+ Parameters
134
+ ----------
135
+ config_dir : Path
136
+ VDYP_CFG directory.
137
+ query : str
138
+ Case-insensitive family, equation-family, or status text.
139
+
140
+ Returns
141
+ -------
142
+ AssistantAnswer
143
+ Matching families and source labels.
144
+ """
145
+ payload = catalog_payload(config_dir)
146
+ rows = payload.get("families")
147
+ families = rows if isinstance(rows, list) else []
148
+ needle = query.strip().lower()
149
+ matches = [
150
+ row
151
+ for row in families
152
+ if isinstance(row, dict)
153
+ and any(
154
+ needle in str(row.get(key, "")).lower()
155
+ for key in ("family", "equation_family", "status")
156
+ )
157
+ ]
158
+ sources = tuple(
159
+ str(row["source_file"])
160
+ for row in matches
161
+ if isinstance(row, dict) and row.get("source_file")
162
+ ) or ("VDYP_CFG",)
163
+ answer = AssistantAnswer(
164
+ f"{len(matches)} families match {query!r}.",
165
+ sources,
166
+ {"families": matches},
167
+ )
168
+ validate_answer(answer)
169
+ return answer
170
+
171
+
172
+ def curve_query(service: CurveService, query: CurveQuery) -> AssistantAnswer:
173
+ """Run a curve query and return a source-backed summary.
174
+
175
+ Parameters
176
+ ----------
177
+ service : CurveService
178
+ Loaded curve service.
179
+ query : CurveQuery
180
+ Query inputs.
181
+
182
+ Returns
183
+ -------
184
+ AssistantAnswer
185
+ Status, point count, and provenance.
186
+ """
187
+ result = service.query(query)
188
+ answer = AssistantAnswer(
189
+ f"{result.status}: {len(result.points)} points. {result.reason}".strip(),
190
+ (result.provenance.source_file, result.provenance.runtime),
191
+ result.to_dict(),
192
+ )
193
+ validate_answer(answer)
194
+ return answer
195
+
196
+
197
+ def provenance_lookup(service: CurveService) -> AssistantAnswer:
198
+ """Return the provenance of the loaded curve table.
199
+
200
+ Parameters
201
+ ----------
202
+ service : CurveService
203
+ Loaded curve service.
204
+
205
+ Returns
206
+ -------
207
+ AssistantAnswer
208
+ Source file, runtime, console version, and schema labels.
209
+ """
210
+ answer = AssistantAnswer(
211
+ f"Source {service.table.source_file}; runtime {service.runtime}.",
212
+ (service.table.source_file, service.runtime),
213
+ {
214
+ "source_file": service.table.source_file,
215
+ "runtime": service.runtime,
216
+ "console_version": service.console_version,
217
+ "control_sequence": service.control_sequence,
218
+ "schema": str(DEFAULT_SCHEMA),
219
+ "aliases": str(DEFAULT_ALIASES),
220
+ },
221
+ )
222
+ validate_answer(answer)
223
+ return answer
224
+
225
+
226
+ def validation_status(matrix: Path, family: str) -> AssistantAnswer:
227
+ """Return the validation status of one family from the matrix.
228
+
229
+ Parameters
230
+ ----------
231
+ matrix : Path
232
+ Equation matrix JSON.
233
+ family : str
234
+ Family name.
235
+
236
+ Returns
237
+ -------
238
+ AssistantAnswer
239
+ Status and evidence, or ``not_implemented`` when the family is absent.
240
+ """
241
+ payload = json.loads(matrix.read_text(encoding="utf-8"))
242
+ rows = payload.get("families") if isinstance(payload, dict) else None
243
+ if not isinstance(rows, list):
244
+ raise AssistantAnswerError("matrix has no families")
245
+ match = next(
246
+ (row for row in rows if isinstance(row, dict) and row.get("family") == family),
247
+ None,
248
+ )
249
+ if match is None:
250
+ answer = AssistantAnswer(
251
+ f"{family}: not_implemented (not in the matrix).",
252
+ (str(matrix),),
253
+ {"family": family, "status": "not_implemented"},
254
+ )
255
+ else:
256
+ answer = AssistantAnswer(
257
+ f"{family}: {match.get('status')}.",
258
+ (str(match.get("evidence", matrix)),),
259
+ match,
260
+ )
261
+ validate_answer(answer)
262
+ return answer