sable-sphynx 0.7.0.dev0__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.
- sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
- sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
- sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
- sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
- sphynx/__init__.py +469 -0
- sphynx/__init__.pyi +376 -0
- sphynx/_analytical_client.py +77 -0
- sphynx/_analytical_values.py +252 -0
- sphynx/_audit_client.py +173 -0
- sphynx/_comparison_adapters.py +113 -0
- sphynx/_componentized_full_rag.py +34 -0
- sphynx/_composed_pipeline.py +109 -0
- sphynx/_full_rag_grading.py +218 -0
- sphynx/_full_rag_trace.py +221 -0
- sphynx/_local_eval.py +268 -0
- sphynx/_pipeline_core.py +243 -0
- sphynx/_pr_client.py +237 -0
- sphynx/_rag_diagnostics_calibration.py +285 -0
- sphynx/_rag_diagnostics_catalog.py +39 -0
- sphynx/_rag_diagnostics_common.py +26 -0
- sphynx/_rag_diagnostics_contracts.py +186 -0
- sphynx/_rag_diagnostics_engine.py +243 -0
- sphynx/_rag_diagnostics_importers.py +242 -0
- sphynx/_rag_diagnostics_ranking.py +81 -0
- sphynx/_rag_diagnostics_semantic.py +325 -0
- sphynx/_rag_ranking.py +149 -0
- sphynx/_speech_benchmark_scoring.py +140 -0
- sphynx/_speech_benchmark_sources.py +275 -0
- sphynx/_speech_complete_scoring.py +218 -0
- sphynx/_speech_complete_source.py +118 -0
- sphynx/_speech_kido.py +294 -0
- sphynx/_speech_scoring.py +77 -0
- sphynx/_track_capture.py +213 -0
- sphynx/analytical_views.py +515 -0
- sphynx/assessment_contracts.py +378 -0
- sphynx/assessment_selection.py +57 -0
- sphynx/audit_access.py +245 -0
- sphynx/audit_contracts.py +204 -0
- sphynx/audit_requests.py +180 -0
- sphynx/authoring.py +188 -0
- sphynx/benchmarks.py +568 -0
- sphynx/candidates.py +62 -0
- sphynx/catalog.py +272 -0
- sphynx/chunking.py +372 -0
- sphynx/clickhouse_reader.py +169 -0
- sphynx/client.py +486 -0
- sphynx/comparison_core.py +149 -0
- sphynx/comparison_views.py +223 -0
- sphynx/components.py +1661 -0
- sphynx/contracts.py +514 -0
- sphynx/customer_rerank.py +299 -0
- sphynx/customer_retrieval.py +146 -0
- sphynx/data/benchmark-releases.v1.json +164 -0
- sphynx/data/public-benchmark-release.v1.json +354 -0
- sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
- sphynx/data/rag-paper-references.v1.json +176 -0
- sphynx/data/rag-paper-schemas.v1.json +1069 -0
- sphynx/data/rag-rerank-v1.json +507 -0
- sphynx/dataset_catalog_data.py +1214 -0
- sphynx/dataset_readiness.py +138 -0
- sphynx/dataset_v3.py +999 -0
- sphynx/demo.py +130 -0
- sphynx/demo_candidates/__init__.py +1 -0
- sphynx/demo_candidates/dense.py +11 -0
- sphynx/demo_candidates/lexical.py +21 -0
- sphynx/diagnostics.py +207 -0
- sphynx/documentation.py +737 -0
- sphynx/documentation_candidate.py +939 -0
- sphynx/evals.py +538 -0
- sphynx/evaluation_protocols.py +124 -0
- sphynx/evaluator_bindings.py +370 -0
- sphynx/evidence.py +352 -0
- sphynx/execution_capabilities.py +109 -0
- sphynx/fast_search.py +285 -0
- sphynx/full_rag.py +341 -0
- sphynx/hosted/__init__.py +18 -0
- sphynx/hosted/benchmark_intake.py +218 -0
- sphynx/hosted/contracts.py +361 -0
- sphynx/hosted/model_admission.py +130 -0
- sphynx/hosted/model_artifacts.py +298 -0
- sphynx/hosted/model_contracts.py +63 -0
- sphynx/hosted/model_gcs.py +181 -0
- sphynx/hosted/model_history.py +84 -0
- sphynx/hosted/model_session.py +233 -0
- sphynx/hosted/public_suite.py +74 -0
- sphynx/hosted/registry.py +97 -0
- sphynx/hosted/revisions.py +126 -0
- sphynx/hosted/sandbox.py +105 -0
- sphynx/hosted/submission.py +287 -0
- sphynx/hosted_workflow.py +451 -0
- sphynx/http_transport.py +86 -0
- sphynx/integrated_rag.py +352 -0
- sphynx/integrated_workflow.py +552 -0
- sphynx/integration_contracts.py +474 -0
- sphynx/invocation.py +791 -0
- sphynx/invocation_candidates.py +392 -0
- sphynx/invocation_catalog_data.py +40 -0
- sphynx/learned_catalog_data.py +30 -0
- sphynx/learned_rerank.py +267 -0
- sphynx/legacy_full_rag.py +67 -0
- sphynx/lifecycle.py +885 -0
- sphynx/lifecycle_workflow.py +490 -0
- sphynx/metric_views.py +474 -0
- sphynx/metrics.py +282 -0
- sphynx/model_adapters.py +438 -0
- sphynx/model_catalog_data.py +30 -0
- sphynx/official_benchmarks.py +385 -0
- sphynx/packs.py +258 -0
- sphynx/pipeline.py +313 -0
- sphynx/pipeline_workflow.py +551 -0
- sphynx/pr_evaluations.py +560 -0
- sphynx/progress.py +89 -0
- sphynx/public_bundle.py +173 -0
- sphynx/public_eval.py +62 -0
- sphynx/public_rerank.py +355 -0
- sphynx/py.typed +0 -0
- sphynx/pytest_plugin.py +62 -0
- sphynx/question_robustness.py +437 -0
- sphynx/rag.py +329 -0
- sphynx/rag_diagnostics.py +309 -0
- sphynx/rag_papers/__init__.py +29 -0
- sphynx/rag_papers/__main__.py +45 -0
- sphynx/rag_papers/candidates.py +19 -0
- sphynx/rag_papers/contracts.py +282 -0
- sphynx/rag_papers/datasets.py +291 -0
- sphynx/rag_papers/experiments.py +116 -0
- sphynx/rag_papers/judges.py +308 -0
- sphynx/rag_papers/registry.py +33 -0
- sphynx/rag_papers/runner.py +176 -0
- sphynx/rag_papers/sable.py +135 -0
- sphynx/rag_papers/schemas.py +12 -0
- sphynx/rag_papers/scoring.py +249 -0
- sphynx/rag_papers/semantic.py +288 -0
- sphynx/rag_papers/trec.py +80 -0
- sphynx/recorded_scores.py +352 -0
- sphynx/records.py +124 -0
- sphynx/reflex_traces.py +193 -0
- sphynx/report_rendering.py +71 -0
- sphynx/rerank_family_contracts.py +150 -0
- sphynx/rerank_workflow.py +424 -0
- sphynx/research_candidate.py +190 -0
- sphynx/results.py +852 -0
- sphynx/retrieval.py +750 -0
- sphynx/retrieval_candidates.py +295 -0
- sphynx/retrieval_catalog_data.py +19 -0
- sphynx/retrieval_recordings.py +125 -0
- sphynx/reviewer_audit.py +266 -0
- sphynx/routing_policies.py +375 -0
- sphynx/runner.py +544 -0
- sphynx/sable_rerank.py +489 -0
- sphynx/score_reliability.py +393 -0
- sphynx/speech_benchmarks.py +204 -0
- sphynx/speech_campaigns.py +378 -0
- sphynx/speech_complete.py +236 -0
- sphynx/speech_contracts.py +225 -0
- sphynx/speech_evidence.py +429 -0
- sphynx/speech_listening.py +115 -0
- sphynx/speech_multilingual.py +71 -0
- sphynx/speech_tts.py +479 -0
- sphynx/sql/001_sync_v1.sql +30 -0
- sphynx/stage_contracts.py +199 -0
- sphynx/statistical_catalog_data.py +53 -0
- sphynx/statistical_rerank.py +232 -0
- sphynx/sync.py +797 -0
- sphynx/workbench.py +378 -0
- sphynx/workbench_catalog_data.py +13 -0
- sphynx/workbench_tracks.py +78 -0
- sphynx/workflow_async.py +197 -0
- sphynx/workflow_contracts.py +594 -0
- sphynx/workflow_decisions.py +211 -0
- sphynx/workflow_defaults.py +16 -0
- sphynx/workflow_extensions.py +212 -0
- sphynx/workflow_registry.py +513 -0
- sphynx/workflows.py +792 -0
sphynx/documentation.py
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
"""Versioned, metadata-only documentation exports.
|
|
2
|
+
|
|
3
|
+
This module uses Python's AST, never imports the inspected SDK, and never runs
|
|
4
|
+
examples, candidate code, providers, models, or evaluation adapters.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import ast
|
|
10
|
+
import copy
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import tomllib
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
EXPORTER_VERSION = "1.4.0"
|
|
21
|
+
SUPPORTED_EXPORTER_VERSIONS = {"1.0.0", "1.1.0", "1.2.0", "1.3.0", EXPORTER_VERSION}
|
|
22
|
+
SOURCE_RECEIPTS_FORMAT = "utf8-lf.v1"
|
|
23
|
+
MAX_BUNDLE_BYTES = 2_000_000
|
|
24
|
+
SCHEMA = "sphynx.docs.bundle.v1"
|
|
25
|
+
MANIFEST_SCHEMA = "sphynx.docs.manifest.v1"
|
|
26
|
+
IMPLEMENTED_IDS = {
|
|
27
|
+
"rag.rerank.v1", "rag.rerank.public.v1", "rag.rerank.sable.dev.v1",
|
|
28
|
+
"rag.rerank.beir.v1", "rag.invoke.v1", "rag.retrieve.v1",
|
|
29
|
+
"rag.question_robustness.v1", "rag.chunk.v1", "rag.pipeline.v1", "rag.full_rag.v1",
|
|
30
|
+
}
|
|
31
|
+
CONTRACT_IDS = {"rag.rerank.v2", "audio.similarity.v1"}
|
|
32
|
+
HOSTED_IDS = {"rag.rerank.v1", "rag.rerank.public.v1"}
|
|
33
|
+
API_MODULES = {
|
|
34
|
+
"client": ["Client.__init__", "Client.evaluate", "Client.list_evals", "Client.describe_eval", "Client.list_benchmarks", "Client.get_benchmark", "Client.get_label_release", "Client.get_experiment", "Client.get_run", "Client.grade", "Client.get_evaluation", "Client.compare_saved", "Client.get_comparison", "Client.leaderboard", "Experiment.wait", "Experiment.refresh", "HostedRun.grade", "HostedEvaluation.wait", "ComparisonRef.result"],
|
|
35
|
+
"packs": ["list_packs", "get_pack", "check_candidate"],
|
|
36
|
+
"evals": ["list_evals", "describe_eval", "resolve_eval"],
|
|
37
|
+
"catalog": ["list_datasets", "get_dataset", "list_candidates"],
|
|
38
|
+
"runner": ["run"], "results": ["grade", "report", "compare"],
|
|
39
|
+
"demo": ["run_demo"],
|
|
40
|
+
"public_rerank": ["run_public_rerank", "grade_public_rerank", "report_public_rerank", "compare_public_rerank"],
|
|
41
|
+
"sable_rerank": ["run_private_rerank", "grade_private_rerank", "compare_private_rerank"],
|
|
42
|
+
"invocation": ["list_invocation_banks", "load_invocation_pack", "run_invocation", "grade_invocation", "report_invocation", "compare_invocation"],
|
|
43
|
+
"retrieval": ["load_retrieval_pack", "run_retrieval", "capture_retrieval", "grade_retrieval", "compare_retrieval"],
|
|
44
|
+
"question_robustness": ["run_question_robustness", "grade_question_robustness", "report_question_robustness", "compare_question_robustness"],
|
|
45
|
+
"chunking": ["run_chunking", "grade_chunking", "report_chunking", "compare_chunking"],
|
|
46
|
+
"pipeline": ["run_pipeline", "grade_pipeline", "report_pipeline", "compare_pipeline"],
|
|
47
|
+
"full_rag": ["run_full_rag", "grade_full_rag", "report_full_rag", "compare_full_rag", "compare_full_rag_contexts", "create_full_rag_review_template"],
|
|
48
|
+
"score_reliability": ["grade_score_reliability", "compare_score_reliability", "evaluate_fitted_calibrator", "report_fitted_calibrator"],
|
|
49
|
+
"reviewer_audit": ["audit_invocation_reviewer"],
|
|
50
|
+
"workbench_tracks": ["run_workbench_pipeline", "run_workbench_full_rag"],
|
|
51
|
+
}
|
|
52
|
+
LEGACY_SOURCE_FILES = sorted({"pyproject.toml", "src/sphynx/__init__.py", "src/sphynx/public_eval.py", *(f"src/sphynx/{name}.py" for name in API_MODULES)})
|
|
53
|
+
PREVIOUS_SOURCE_FILES = sorted(set(LEGACY_SOURCE_FILES) | {"src/sphynx/rag_diagnostics.py", *("src/sphynx/_rag_diagnostics_" + name + ".py" for name in ("common", "contracts", "ranking", "semantic", "engine", "catalog", "calibration", "importers"))})
|
|
54
|
+
# Additive SDK 0.5.0 APIs; explicit namespaces prevent false root exports.
|
|
55
|
+
API_MODULES.update({'rag_papers/contracts': ['make_case', 'make_trace', 'judgment', 'candidate_input', 'validate_case', 'validate_trace', 'validate_judgment'], 'rag_papers/datasets': ['prepare_paper_dataset', 'prepare_official_paper_dataset', 'prepare_canonical_paper_dataset', 'load_paper_dataset', 'PaperDataset.cases'], 'rag_papers/registry': ['describe_paper_methods'], 'rag_papers/scoring': ['evaluate_beir', 'evaluate_hotpotqa', 'evaluate_musique', 'evaluate_qasper', 'evaluate_rgb', 'evaluate_crag'], 'rag_papers/semantic': ['evaluate_sufficient_context', 'evaluate_ragas', 'evaluate_alce', 'evaluate_ares', 'evaluate_factscore', 'evaluate_ragchecker', 'evaluate_lost_in_the_middle', 'evaluate_trec_rag_2025'], 'rag_papers/judges': ['JsonJudgeAdapter.__init__', 'JudgeSession.__init__', 'run_paper_judge', 'prepare_ares_training', 'train_ares_judge'], 'rag_papers/candidates': ['candidate_trace'], 'rag_papers/runner': ['capture_paper_candidate', 'run_paper_suite', 'load_paper_run', 'compare_paper_runs'], 'rag_papers/experiments': ['plan_lost_in_the_middle', 'plan_rgb', 'plan_musique', 'prepare_intervention_dataset'], 'rag_papers/sable': ['prepare_sable_paper_dataset', 'evaluate_sable_evidence', 'bind_reflex_case_history', 'trace_from_level3', 'trace_from_full_rag_capture'], 'rag_papers/schemas': ['get_paper_schema'], 'rag_papers/trec': ['prepare_trec_rag_2025_dataset', 'trace_from_trec_rag_2025'], 'reflex_traces': ['load_reflex_traces', 'pair_reflex_traces'], 'official_benchmarks': ['list_official_benchmarks', 'fetch_official_benchmark_source', 'prepare_official_benchmark', 'load_official_benchmark'], 'speech_contracts': ['require', 'encode', 'sha', 'parse', 'fields', 'identifier', 'checksum', 'validate_method', 'validate_request', 'decode_audio', 'serve_tts'], 'speech_evidence': ['import_kido_speech_evidence', 'load_speech_evidence', 'describe_speech_suite', 'speech_candidate_view', 'grade_speech_evidence', 'load_speech_evaluation'], 'speech_tts': ['describe_tts_suite', 'freeze_tts_plan', 'run_tts_plan', 'grade_tts_captures', 'load_tts_run']})
|
|
56
|
+
PUBLIC_API_NAMESPACES = {'rag_papers/contracts': 'sphynx.rag_papers', 'rag_papers/datasets': 'sphynx.rag_papers', 'rag_papers/registry': 'sphynx.rag_papers', 'rag_papers/scoring': 'sphynx.rag_papers', 'rag_papers/semantic': 'sphynx.rag_papers', 'rag_papers/judges': 'sphynx.rag_papers', 'rag_papers/candidates': 'sphynx.rag_papers', 'rag_papers/runner': 'sphynx.rag_papers', 'rag_papers/experiments': 'sphynx.rag_papers', 'rag_papers/sable': 'sphynx.rag_papers', 'rag_papers/schemas': 'sphynx.rag_papers', 'rag_papers/trec': 'sphynx.rag_papers', 'reflex_traces': 'sphynx.reflex_traces', 'official_benchmarks': 'sphynx.official_benchmarks', 'speech_contracts': 'sphynx.speech_contracts', 'speech_evidence': 'sphynx.speech_evidence', 'speech_tts': 'sphynx.speech_tts'}
|
|
57
|
+
SOURCE_FILES = sorted({"pyproject.toml", "src/sphynx/__init__.py", "src/sphynx/public_eval.py", *(f"src/sphynx/{name}.py" for name in API_MODULES)})
|
|
58
|
+
API_MODULES["rag_diagnostics"] = ['load_rag_diagnostics_dataset', 'validate_rag_diagnostics_dataset', 'rag_candidate_view', 'grade_rag_diagnostics', 'load_rag_diagnostics_demo', 'list_rag_diagnostics_metrics', 'rag_diagnostics_capabilities', 'save_rag_diagnostics_report', 'load_rag_diagnostics_report', 'render_rag_diagnostics_report', 'import_rag_diagnostics_beir', 'audit_rag_judge', 'estimate_rag_ppi_mean', 'create_rag_diagnostics_review_template']
|
|
59
|
+
SOURCE_FILES = sorted({"pyproject.toml", "src/sphynx/__init__.py", "src/sphynx/public_eval.py", *(f"src/sphynx/{name}.py" for name in API_MODULES)})
|
|
60
|
+
SOURCE_FILES = sorted(SOURCE_FILES + ['src/sphynx/_rag_diagnostics_common.py', 'src/sphynx/_rag_diagnostics_contracts.py', 'src/sphynx/_rag_diagnostics_ranking.py', 'src/sphynx/_rag_diagnostics_semantic.py', 'src/sphynx/_rag_diagnostics_engine.py', 'src/sphynx/_rag_diagnostics_catalog.py', 'src/sphynx/_rag_diagnostics_calibration.py', 'src/sphynx/_rag_diagnostics_importers.py'])
|
|
61
|
+
KEYS = {
|
|
62
|
+
"payload": "schema_version release pages suites api families methods datasets controls evidence versions known_gaps reconciliation references".split(),
|
|
63
|
+
"manifest": "schema_version content_sha256 content_bytes catalog_sha256 source_receipts source_receipts_format source_receipts_sha256 build_time_utc exporter_sha256 references_sha256 documentation_source_receipts documentation_source_receipts_sha256".split(),
|
|
64
|
+
"release": "id label sdk_version source_commit source_tree source_repository wheel_sha256 exporter_version hosted_capability".split(),
|
|
65
|
+
"capability": "observed_on identity hosted_suite_ids".split(),
|
|
66
|
+
"page": "id title category summary sections".split(),
|
|
67
|
+
"section": "id title paragraphs blocks tables links".split(),
|
|
68
|
+
"block": "language code execution".split(), "table": ["columns", "rows"], "link": ["label", "url"],
|
|
69
|
+
"suite": "id title status local_api hosted_admitted boundary data_requirements label_authority metrics api_ids known_gaps source_url parameters examples returns failures evidence_ids".split(),
|
|
70
|
+
"metric": ["id", "denominator"],
|
|
71
|
+
"suite_parameter": "name type required default accepted description".split(),
|
|
72
|
+
"api": "id module signature parameters returns summary source_url".split(),
|
|
73
|
+
"api_parameter": "name type kind required default description accepted".split(),
|
|
74
|
+
"family": "id title pr_url head_sha source_url entry_count payload_sha256 source_inventory_sha256 hosted_admitted hosted_root superseded".split(),
|
|
75
|
+
"hosted_root": "method runtime_profile threads suite baseline admitted fresh_execution_verified".split(),
|
|
76
|
+
"superseded": "pr_url head_sha source_url".split(),
|
|
77
|
+
"method": "id family name algorithm aliases execution_modes status pr_url head_sha source_url parameters prerequisites known_gaps model configuration_digest hosted_admitted supported_evaluations".split(),
|
|
78
|
+
"model": "checkpoint revision tokenizer_revision backend device threads max_length batch_size activation truncation seed".split(),
|
|
79
|
+
"dataset": "id suite_id release population split case_count source_case_count review_status status reason previously_exercised access_requirements source_url".split(),
|
|
80
|
+
"control": "id title methods pr_url head_sha source_url status".split(),
|
|
81
|
+
"evidence": "id title type status summary counts source_url source_commit limitations experiment_id run_id evaluation_id requires_ownership metrics runtime".split(),
|
|
82
|
+
"version": "id title summary source_url".split(),
|
|
83
|
+
"reconciliation": "discovery suite_ids missing_suite_ids explanation".split(),
|
|
84
|
+
"references": "families methods concepts".split(),
|
|
85
|
+
"reference_family": "id title summary purpose changes notes".split(),
|
|
86
|
+
"reference_method": "id title summary purpose canonical_id steps equations configuration differences notes".split(),
|
|
87
|
+
"reference_concept": "id title summary paragraphs equations".split(),
|
|
88
|
+
"equation": "latex explanation symbols".split(),
|
|
89
|
+
"equation_symbol": "symbol description".split(),
|
|
90
|
+
"reference_configuration": "name value description".split(),
|
|
91
|
+
}
|
|
92
|
+
FORBIDDEN_KEY = re.compile(r"^(?:token|password|secret|credential|raw_questions?|raw_labels?|answer_capture|weights?|fitted_state|gcs_uri|internal_path|environment)$", re.I)
|
|
93
|
+
FORBIDDEN_TEXT = re.compile(r"(?:gs://|[A-Za-z]:[\\/](?:Users|ProgramData)[\\/]|(?:^|[\\/])\.agent[\\/]|<\s*(?:script|iframe|object|svg)\b|javascript:|-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|sk-[A-Za-z0-9]{20,})", re.I)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DocumentationError(ValueError):
|
|
97
|
+
"""Invalid, stale, incompatible, or unapproved documentation metadata."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def canonical_bytes(value: Any) -> bytes:
|
|
101
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def sha256(value: bytes) -> str:
|
|
105
|
+
return hashlib.sha256(value).hexdigest()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _keys(value: Any, kind: str, *, optional: tuple[str, ...] = ()) -> None:
|
|
109
|
+
if not isinstance(value, dict):
|
|
110
|
+
raise DocumentationError(f"{kind} must be an object")
|
|
111
|
+
allowed = set(KEYS[kind])
|
|
112
|
+
missing = allowed - set(value) - set(optional)
|
|
113
|
+
unknown = set(value) - allowed
|
|
114
|
+
if missing or unknown:
|
|
115
|
+
raise DocumentationError(f"{kind} keys: missing={sorted(missing)}, unapproved={sorted(unknown)}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _hash(value: Any, name: str, length: int = 64) -> None:
|
|
119
|
+
if not isinstance(value, str) or not re.fullmatch(rf"[0-9a-f]{{{length}}}", value):
|
|
120
|
+
raise DocumentationError(f"{name} must be a complete {length}-character lowercase hash")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _strings(value: Any, name: str) -> None:
|
|
124
|
+
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
|
125
|
+
raise DocumentationError(f"{name} must be an array of strings")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _confidentiality(value: Any, path: str = "payload") -> None:
|
|
129
|
+
if isinstance(value, dict):
|
|
130
|
+
for key, item in value.items():
|
|
131
|
+
if FORBIDDEN_KEY.fullmatch(key):
|
|
132
|
+
raise DocumentationError(f"unapproved internal field: {path}.{key}")
|
|
133
|
+
_confidentiality(item, f"{path}.{key}")
|
|
134
|
+
elif isinstance(value, list):
|
|
135
|
+
for index, item in enumerate(value):
|
|
136
|
+
_confidentiality(item, f"{path}[{index}]")
|
|
137
|
+
elif isinstance(value, str) and FORBIDDEN_TEXT.search(value):
|
|
138
|
+
raise DocumentationError(f"unapproved private locator or executable content in {path}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _unique(items: Any, name: str, key: str = "id") -> set[str]:
|
|
142
|
+
if not isinstance(items, list):
|
|
143
|
+
raise DocumentationError(f"{name} must be an array")
|
|
144
|
+
ids = [item.get(key) if isinstance(item, dict) else None for item in items]
|
|
145
|
+
if any(not isinstance(item, str) or not item or len(item) > 256 for item in ids) or len(ids) != len(set(ids)):
|
|
146
|
+
raise DocumentationError(f"{name} has missing/invalid/duplicate IDs")
|
|
147
|
+
return set(ids)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _url(value: Any) -> None:
|
|
151
|
+
if not isinstance(value, str) or not re.match(r"^https://(?:github\.com/sable-inc/|huggingface\.co/docs/|docs\.pytorch\.org/)", value):
|
|
152
|
+
raise DocumentationError("source/link URL must use an approved HTTPS source")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _guide_link(value: Any, pages: dict) -> None:
|
|
156
|
+
if isinstance(value, str) and value.startswith("#"):
|
|
157
|
+
match = re.fullmatch(r"#([a-zA-Z0-9._-]+)(?:/([a-zA-Z0-9._-]+))?", value)
|
|
158
|
+
target = pages.get(match[1]) if match else None
|
|
159
|
+
if target is None or (match[2] and not any(section["id"] == match[2] for section in target["sections"])):
|
|
160
|
+
raise DocumentationError("internal guide link must identify an existing page and section")
|
|
161
|
+
else:
|
|
162
|
+
_url(value)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _block(value: Any) -> None:
|
|
166
|
+
_keys(value, "block")
|
|
167
|
+
if value["language"] not in {"python", "powershell", "bash", "text"} or value["execution"] not in {"fixture", "read_only", "paid_remote", "metadata_only"}:
|
|
168
|
+
raise DocumentationError("unknown example language/execution boundary")
|
|
169
|
+
if not isinstance(value["code"], str) or value["code"].startswith((">>>", "PS ", "$ ")):
|
|
170
|
+
raise DocumentationError("copyable examples must contain code without prompts")
|
|
171
|
+
if value["language"] == "python":
|
|
172
|
+
try:
|
|
173
|
+
ast.parse(value["code"])
|
|
174
|
+
except SyntaxError as error:
|
|
175
|
+
raise DocumentationError(f"example syntax: {error.msg}") from error
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _text(value: Any, name: str) -> None:
|
|
179
|
+
if not isinstance(value, str) or not value.strip() or len(value) > 20_000:
|
|
180
|
+
raise DocumentationError(f"{name} must be a nonempty bounded string")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _equations(values: Any) -> None:
|
|
184
|
+
if not isinstance(values, list):
|
|
185
|
+
raise DocumentationError("equations must be an array")
|
|
186
|
+
for equation in values:
|
|
187
|
+
_keys(equation, "equation")
|
|
188
|
+
_text(equation["latex"], "equation LaTeX")
|
|
189
|
+
_text(equation["explanation"], "equation explanation")
|
|
190
|
+
_unique(equation["symbols"], "equation symbols", "symbol")
|
|
191
|
+
for symbol in equation["symbols"]:
|
|
192
|
+
_keys(symbol, "equation_symbol")
|
|
193
|
+
_text(symbol["description"], "symbol description")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def validate_references(references: Any, payload: dict[str, Any]) -> None:
|
|
197
|
+
"""Check explanatory coverage while retaining exact historical selectors."""
|
|
198
|
+
_keys(references, "references")
|
|
199
|
+
_confidentiality(references, "references")
|
|
200
|
+
families = _unique(references["families"], "reference families")
|
|
201
|
+
methods = _unique(references["methods"], "reference methods")
|
|
202
|
+
_unique(references["concepts"], "reference concepts")
|
|
203
|
+
if families != {item["id"] for item in payload["families"]} or methods != {item["id"] for item in payload["methods"]}:
|
|
204
|
+
raise DocumentationError("reference coverage must retain every catalog family and method ID")
|
|
205
|
+
method_families = {item["id"]: item["family"] for item in payload["methods"]}
|
|
206
|
+
reference_methods = {item["id"]: item for item in references["methods"]}
|
|
207
|
+
for item in references["methods"]:
|
|
208
|
+
_keys(item, "reference_method")
|
|
209
|
+
for kind, collection in (("reference_family", "families"), ("reference_method", "methods"), ("reference_concept", "concepts")):
|
|
210
|
+
for item in references[collection]:
|
|
211
|
+
_keys(item, kind)
|
|
212
|
+
for field in ("title", "summary"):
|
|
213
|
+
_text(item[field], f"{kind} {field}")
|
|
214
|
+
if collection != "concepts":
|
|
215
|
+
_text(item["purpose"], f"{kind} purpose")
|
|
216
|
+
for field in ("changes", "notes", "steps", "differences", "paragraphs"):
|
|
217
|
+
if field in item:
|
|
218
|
+
_strings(item[field], f"{kind} {field}")
|
|
219
|
+
for value in item[field]:
|
|
220
|
+
_text(value, f"{kind} {field}")
|
|
221
|
+
if "equations" in item:
|
|
222
|
+
_equations(item["equations"])
|
|
223
|
+
if collection == "methods":
|
|
224
|
+
canonical = item["canonical_id"]
|
|
225
|
+
if not isinstance(canonical, str) or canonical not in methods or method_families[canonical] != method_families[item["id"]]:
|
|
226
|
+
raise DocumentationError("canonical method must be an existing ID in the same family")
|
|
227
|
+
if reference_methods[canonical]["canonical_id"] != canonical:
|
|
228
|
+
raise DocumentationError("canonical method must resolve directly to a canonical record")
|
|
229
|
+
_unique(item["configuration"], "reference configuration", "name")
|
|
230
|
+
for parameter in item["configuration"]:
|
|
231
|
+
_keys(parameter, "reference_configuration")
|
|
232
|
+
_text(parameter["value"], "configuration value")
|
|
233
|
+
_text(parameter["description"], "configuration description")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def validate_payload(payload: dict[str, Any]) -> None:
|
|
237
|
+
"""Validate the narrow public contract, identity, references, and status fixtures."""
|
|
238
|
+
_keys(payload, "payload", optional=("references",))
|
|
239
|
+
if payload["schema_version"] != SCHEMA:
|
|
240
|
+
raise DocumentationError("unsupported documentation schema")
|
|
241
|
+
if len(canonical_bytes(payload)) > MAX_BUNDLE_BYTES:
|
|
242
|
+
raise DocumentationError("documentation payload exceeds size limit")
|
|
243
|
+
_confidentiality(payload)
|
|
244
|
+
release = payload["release"]
|
|
245
|
+
_keys(release, "release")
|
|
246
|
+
if not isinstance(release["id"], str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", release["id"]):
|
|
247
|
+
raise DocumentationError("release ID must be a stable bounded identifier")
|
|
248
|
+
_hash(release["source_commit"], "source commit", 40)
|
|
249
|
+
_hash(release["source_tree"], "source tree", 40)
|
|
250
|
+
_hash(release["wheel_sha256"], "wheel digest")
|
|
251
|
+
if release["source_repository"] != "https://github.com/sable-inc/sphynx" or release["exporter_version"] not in SUPPORTED_EXPORTER_VERSIONS:
|
|
252
|
+
raise DocumentationError("incompatible documentation source/exporter")
|
|
253
|
+
_keys(release["hosted_capability"], "capability")
|
|
254
|
+
capability = release["hosted_capability"]
|
|
255
|
+
if not isinstance(capability["observed_on"], str) or not re.fullmatch(r"\d{4}-\d{2}-\d{2}", capability["observed_on"]) or not isinstance(capability["identity"], str):
|
|
256
|
+
raise DocumentationError("hosted capability needs an explicit observation date and identity")
|
|
257
|
+
_strings(capability["hosted_suite_ids"], "hosted suite IDs")
|
|
258
|
+
if set(release["hosted_capability"]["hosted_suite_ids"]) != HOSTED_IDS:
|
|
259
|
+
raise DocumentationError("hosted availability must describe exactly the two admitted suites")
|
|
260
|
+
ids = {name: _unique(payload[name], name) for name in ("pages", "suites", "api", "families", "methods", "datasets", "controls", "evidence", "versions")}
|
|
261
|
+
if ids["suites"] != IMPLEMENTED_IDS | CONTRACT_IDS:
|
|
262
|
+
raise DocumentationError("suite inventory must reconcile all ten implemented and two contract-only IDs")
|
|
263
|
+
if len(payload["methods"]) != 85 or {f["id"]: f["entry_count"] for f in payload["families"]} != {"statistical": 50, "learned": 9, "pointwise": 5, "invocation": 14, "retrieval": 7}:
|
|
264
|
+
raise DocumentationError("research catalogue must retain 85 entries across five families; entries are not fresh models")
|
|
265
|
+
page_map = {page["id"]: page for page in payload["pages"]}
|
|
266
|
+
for page in payload["pages"]:
|
|
267
|
+
_keys(page, "page")
|
|
268
|
+
_unique(page["sections"], "page sections")
|
|
269
|
+
for section in page["sections"]:
|
|
270
|
+
_keys(section, "section")
|
|
271
|
+
if not all(isinstance(p, str) for p in section["paragraphs"]):
|
|
272
|
+
raise DocumentationError("paragraphs must be plain strings")
|
|
273
|
+
for block in section["blocks"]:
|
|
274
|
+
_block(block)
|
|
275
|
+
for table in section["tables"]:
|
|
276
|
+
_keys(table, "table")
|
|
277
|
+
_strings(table["columns"], "table columns")
|
|
278
|
+
for row in table["rows"]:
|
|
279
|
+
_strings(row, "table row")
|
|
280
|
+
if not table["columns"] or any(len(row) != len(table["columns"]) for row in table["rows"]):
|
|
281
|
+
raise DocumentationError("table rows must match column count")
|
|
282
|
+
for link in section["links"]:
|
|
283
|
+
_keys(link, "link")
|
|
284
|
+
_guide_link(link["url"], page_map)
|
|
285
|
+
for suite in payload["suites"]:
|
|
286
|
+
_keys(suite, "suite")
|
|
287
|
+
for field in ("data_requirements", "api_ids", "known_gaps", "returns", "failures", "evidence_ids"):
|
|
288
|
+
_strings(suite[field], f"suite {field}")
|
|
289
|
+
if type(suite["local_api"]) is not bool or type(suite["hosted_admitted"]) is not bool:
|
|
290
|
+
raise DocumentationError("suite availability requires explicit booleans")
|
|
291
|
+
expected_status = "implemented" if suite["id"] in IMPLEMENTED_IDS else "contract_only"
|
|
292
|
+
if suite["status"] != expected_status or suite["hosted_admitted"] != (suite["id"] in HOSTED_IDS) or (suite["id"] in CONTRACT_IDS and suite["local_api"]):
|
|
293
|
+
raise DocumentationError(f"suite status/admission regression: {suite['id']}")
|
|
294
|
+
if not set(suite["api_ids"]) <= ids["api"] or not set(suite["evidence_ids"]) <= ids["evidence"]:
|
|
295
|
+
raise DocumentationError(f"missing API/evidence reference: {suite['id']}")
|
|
296
|
+
_url(suite["source_url"])
|
|
297
|
+
for metric in suite["metrics"]:
|
|
298
|
+
_keys(metric, "metric")
|
|
299
|
+
for parameter in suite["parameters"]:
|
|
300
|
+
_keys(parameter, "suite_parameter")
|
|
301
|
+
_strings(parameter["accepted"], "suite accepted values")
|
|
302
|
+
for example in suite["examples"]:
|
|
303
|
+
_block(example)
|
|
304
|
+
for interface in payload["api"]:
|
|
305
|
+
_keys(interface, "api")
|
|
306
|
+
_url(interface["source_url"])
|
|
307
|
+
names = set()
|
|
308
|
+
for parameter in interface["parameters"]:
|
|
309
|
+
_keys(parameter, "api_parameter")
|
|
310
|
+
_strings(parameter["accepted"], "API accepted values")
|
|
311
|
+
if parameter["name"] in names:
|
|
312
|
+
raise DocumentationError("duplicate API parameter")
|
|
313
|
+
names.add(parameter["name"])
|
|
314
|
+
for family in payload["families"]:
|
|
315
|
+
_keys(family, "family")
|
|
316
|
+
_hash(family["head_sha"], "family head", 40)
|
|
317
|
+
_url(family["source_url"])
|
|
318
|
+
_keys(family["superseded"], "superseded")
|
|
319
|
+
if family["hosted_root"] is not None:
|
|
320
|
+
_keys(family["hosted_root"], "hosted_root")
|
|
321
|
+
if family["hosted_admitted"] is not False:
|
|
322
|
+
raise DocumentationError("research factory metadata is not family hosted admission")
|
|
323
|
+
if sum(method["family"] == family["id"] for method in payload["methods"]) != family["entry_count"]:
|
|
324
|
+
raise DocumentationError("family entry count does not match exact method inventory")
|
|
325
|
+
family_map = {family["id"]: family for family in payload["families"]}
|
|
326
|
+
for method in payload["methods"]:
|
|
327
|
+
_keys(method, "method")
|
|
328
|
+
if method["family"] not in ids["families"] or not set(method["supported_evaluations"]) <= ids["suites"]:
|
|
329
|
+
raise DocumentationError("method has missing family/evaluation reference")
|
|
330
|
+
_hash(method["head_sha"], "method head", 40)
|
|
331
|
+
family = family_map[method["family"]]
|
|
332
|
+
if method["head_sha"] != family["head_sha"] or method["pr_url"] != family["pr_url"]:
|
|
333
|
+
raise DocumentationError("method identity does not match its frozen family observation")
|
|
334
|
+
for field in ("aliases", "execution_modes", "prerequisites", "known_gaps", "supported_evaluations"):
|
|
335
|
+
_strings(method[field], f"method {field}")
|
|
336
|
+
_url(method["source_url"])
|
|
337
|
+
if method["hosted_admitted"] is not False:
|
|
338
|
+
raise DocumentationError("research method profiles are not the separately admitted hosted root")
|
|
339
|
+
if method["name"] in {"aidan-production", "aidan-gpt6sol-production", "binary-cascades"} and method["status"] != "blocked":
|
|
340
|
+
raise DocumentationError("unverified production/binary invocation methods must remain blocked")
|
|
341
|
+
if method["model"] is not None:
|
|
342
|
+
_keys(method["model"], "model")
|
|
343
|
+
# Method parameters are the only extensible object: scalar defaults with
|
|
344
|
+
# stable bounded names, never arbitrary nested metadata or asset blobs.
|
|
345
|
+
allowed_parameters = {"s5_minilm_weight", "s5_lexical_weight", "selection_digest"} if method["name"] in {"S5a", "S5b", "S5c"} else set()
|
|
346
|
+
if not isinstance(method["parameters"], dict) or not set(method["parameters"]) <= allowed_parameters or any(not isinstance(value, str) for value in method["parameters"].values()):
|
|
347
|
+
raise DocumentationError("method parameters must be approved scalar configuration defaults")
|
|
348
|
+
for name, kind in (("datasets", "dataset"), ("controls", "control"), ("evidence", "evidence"), ("versions", "version")):
|
|
349
|
+
for item in payload[name]:
|
|
350
|
+
_keys(item, kind)
|
|
351
|
+
_url(item["source_url"])
|
|
352
|
+
if kind == "dataset" and item["suite_id"] not in ids["suites"] | {"rag.dataset.v3"}:
|
|
353
|
+
raise DocumentationError("dataset has missing suite reference")
|
|
354
|
+
if kind == "evidence":
|
|
355
|
+
for field in ("counts", "metrics", "runtime"):
|
|
356
|
+
if not isinstance(item[field], dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in item[field].items()):
|
|
357
|
+
raise DocumentationError("evidence metadata maps must contain approved string summaries")
|
|
358
|
+
for row in payload["reconciliation"]:
|
|
359
|
+
_keys(row, "reconciliation")
|
|
360
|
+
if not set(row["suite_ids"]) <= ids["suites"] or not set(row["missing_suite_ids"]) <= ids["suites"]:
|
|
361
|
+
raise DocumentationError("unknown suite in discovery reconciliation")
|
|
362
|
+
if "references" in payload:
|
|
363
|
+
validate_references(payload["references"], payload)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _read_json(path: Path) -> Any:
|
|
367
|
+
raw = path.read_bytes()
|
|
368
|
+
if len(raw) > MAX_BUNDLE_BYTES:
|
|
369
|
+
raise DocumentationError(f"oversized JSON: {path.name}")
|
|
370
|
+
def no_duplicates(pairs):
|
|
371
|
+
obj = {}
|
|
372
|
+
for key, value in pairs:
|
|
373
|
+
if key in obj:
|
|
374
|
+
raise DocumentationError(f"duplicate JSON key: {key}")
|
|
375
|
+
obj[key] = value
|
|
376
|
+
return obj
|
|
377
|
+
try:
|
|
378
|
+
return json.loads(raw.decode("utf-8-sig"), object_pairs_hook=no_duplicates, parse_constant=lambda v: (_ for _ in ()).throw(DocumentationError(f"invalid numeric constant: {v}")))
|
|
379
|
+
except (ValueError, UnicodeError) as error:
|
|
380
|
+
raise DocumentationError(f"invalid JSON in {path.name}: {error}") from error
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _trees(root: Path) -> dict[str, ast.Module]:
|
|
384
|
+
return {module: ast.parse((root / f"src/sphynx/{module}.py").read_text(encoding="utf-8-sig"), filename=f"{module}.py") for module in API_MODULES}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _find(tree: ast.Module, name: str) -> ast.FunctionDef:
|
|
388
|
+
pieces = name.split(".")
|
|
389
|
+
nodes = tree.body
|
|
390
|
+
for piece in pieces:
|
|
391
|
+
matches = [node for node in nodes if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.name == piece]
|
|
392
|
+
if len(matches) != 1:
|
|
393
|
+
raise DocumentationError(f"missing or ambiguous public API: {name}")
|
|
394
|
+
found = matches[0]
|
|
395
|
+
nodes = found.body
|
|
396
|
+
if not isinstance(found, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
397
|
+
raise DocumentationError(f"API is not a function: {name}")
|
|
398
|
+
return found
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _interface(module: str, name: str, node: ast.FunctionDef, commit: str, descriptions: dict) -> dict:
|
|
402
|
+
args = copy.deepcopy(node.args)
|
|
403
|
+
if "." in name and args.args and args.args[0].arg in {"self", "cls"}:
|
|
404
|
+
args.args.pop(0)
|
|
405
|
+
required_count = len(args.posonlyargs) + len(args.args) - len(args.defaults)
|
|
406
|
+
positions = [(arg, "positional_only" if index < len(args.posonlyargs) else "positional_or_keyword", None if index < required_count else args.defaults[index-required_count], index < required_count) for index, arg in enumerate([*args.posonlyargs, *args.args])]
|
|
407
|
+
if args.vararg:
|
|
408
|
+
positions.append((args.vararg, "var_positional", None, False))
|
|
409
|
+
positions.extend((arg, "keyword_only", default, default is None) for arg, default in zip(args.kwonlyargs, args.kw_defaults))
|
|
410
|
+
if args.kwarg:
|
|
411
|
+
positions.append((args.kwarg, "var_keyword", None, False))
|
|
412
|
+
public_module = PUBLIC_API_NAMESPACES.get(module)
|
|
413
|
+
identity = f"{public_module}.{name}" if public_module else f"sphynx.{name}"
|
|
414
|
+
curated = descriptions.get(identity, {})
|
|
415
|
+
parameters = []
|
|
416
|
+
for arg, kind, default, required in positions:
|
|
417
|
+
explanation = curated.get("parameters", {}).get(arg.arg, {})
|
|
418
|
+
parameters.append({"name": arg.arg, "type": ast.unparse(arg.annotation) if arg.annotation else "Unannotated", "kind": kind, "required": required, "default": ast.unparse(default) if default is not None else None, "description": explanation.get("description", _parameter_help(arg.arg)), "accepted": explanation.get("accepted", [])})
|
|
419
|
+
returns = ast.unparse(node.returns) if node.returns else "Unannotated"
|
|
420
|
+
return {"id": identity, "module": public_module or f"sphynx.{module}", "signature": f"{name}({ast.unparse(args)})" + (f" -> {returns}" if node.returns else ""), "parameters": parameters, "returns": returns, "summary": curated.get("summary", (ast.get_docstring(node) or "See the measured boundary, input requirements, and known gaps for the related suite.").split("\n")[0]), "source_url": f"https://github.com/sable-inc/sphynx/blob/{commit}/src/sphynx/{module}.py#L{node.lineno}"}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _parameter_help(name: str) -> str:
|
|
424
|
+
explanations = {
|
|
425
|
+
"candidate_pr": "Allowed candidate GitHub PR URL. Hosted admission is checked independently.",
|
|
426
|
+
"expected_head_sha": "Complete current PR head guard; it does not select a historical commit.",
|
|
427
|
+
"expected_baseline_sha": "Complete main baseline head guard, currently restricted to registered-main-reflex-lexical:v1.",
|
|
428
|
+
"candidate_method": "Optional family selector. The admitted MiniLM root uses no selector; other research families are not hosted admitted.",
|
|
429
|
+
"suite": "Evaluation ID; hosted submission admits only rag.rerank.v1 and rag.rerank.public.v1.",
|
|
430
|
+
"baseline": "Separate candidate baseline session. supplied-score:v1 uses provided BM25 scores, without fresh retrieval.",
|
|
431
|
+
"parameters": "Protocol-specific allowlisted parameters; undocumented keys fail contract resolution.",
|
|
432
|
+
"expected_sha256": "Pinned approved input digest; metadata does not grant private data access.",
|
|
433
|
+
"dataset_path": "Owner-provided approved dataset file; raw data stays outside documentation artifacts.",
|
|
434
|
+
"output_dir": "Local output directory for saved captures/reports.",
|
|
435
|
+
"adapter": "Explicit local trusted adapter; local execution is not hosted isolation.",
|
|
436
|
+
"synthetic": "Use only explicit synthetic fixtures where supported.",
|
|
437
|
+
"judgments": "Independent capture-bound review. Missing review remains unavailable.",
|
|
438
|
+
"on_progress": "Optional callback for progress messages while waiting.",
|
|
439
|
+
"timeout": "Maximum wait/request duration; failures propagate as documented client errors.",
|
|
440
|
+
"token": "Read from existing protected configuration; never publish credentials or put them in example command arguments.",
|
|
441
|
+
}
|
|
442
|
+
return explanations.get(name, "Declared SDK argument. Follow the pinned source type/default and the suite's data and eligibility requirements.")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def extract_api(root: Path, commit: str, descriptions: dict | None = None) -> list[dict]:
|
|
446
|
+
trees = _trees(root)
|
|
447
|
+
return sorted((_interface(module, name, _find(trees[module], name), commit, descriptions or {}) for module, names in API_MODULES.items() for name in names), key=lambda item: item["id"])
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _definition_statuses(root: Path) -> dict[str, str]:
|
|
451
|
+
statuses = {}
|
|
452
|
+
for module in ("evals", "public_eval"):
|
|
453
|
+
tree = ast.parse((root / f"src/sphynx/{module}.py").read_text(encoding="utf-8-sig"))
|
|
454
|
+
literals = {node.targets[0].id: node.value.value for node in tree.body if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name) and isinstance(node.value, ast.Constant)}
|
|
455
|
+
for node in ast.walk(tree):
|
|
456
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "EvaluationDefinition" and len(node.args) >= 4:
|
|
457
|
+
identity = node.args[0].value if isinstance(node.args[0], ast.Constant) else literals.get(node.args[0].id) if isinstance(node.args[0], ast.Name) else None
|
|
458
|
+
status = node.args[3].value if isinstance(node.args[3], ast.Constant) else None
|
|
459
|
+
if identity and status:
|
|
460
|
+
statuses[identity] = status
|
|
461
|
+
return statuses
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _metadata_expression(node: ast.AST, names: dict[str, Any]) -> Any:
|
|
465
|
+
"""Interpret only literal schema constructors, never Python or SDK code."""
|
|
466
|
+
if isinstance(node, ast.Constant):
|
|
467
|
+
return node.value
|
|
468
|
+
if isinstance(node, (ast.List, ast.Tuple)):
|
|
469
|
+
return [_metadata_expression(item, names) for item in node.elts]
|
|
470
|
+
if isinstance(node, ast.Dict):
|
|
471
|
+
return {_metadata_expression(key, names): _metadata_expression(value, names) for key, value in zip(node.keys, node.values)}
|
|
472
|
+
if isinstance(node, ast.Name) and node.id in names:
|
|
473
|
+
return names[node.id]
|
|
474
|
+
if isinstance(node, ast.Subscript):
|
|
475
|
+
return _metadata_expression(node.value, names)[_metadata_expression(node.slice, names)]
|
|
476
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in {"_object", "_integer", "_reporting", "_copy"}:
|
|
477
|
+
arguments = [_metadata_expression(arg, names) for arg in node.args]
|
|
478
|
+
keywords = {item.arg: _metadata_expression(item.value, names) for item in node.keywords}
|
|
479
|
+
if node.func.id == "_object":
|
|
480
|
+
schema = {"type": "object", "properties": arguments[0], "required": keywords.get("required", []), "additionalProperties": False}
|
|
481
|
+
if "default" in keywords:
|
|
482
|
+
schema["default"] = keywords["default"]
|
|
483
|
+
return schema
|
|
484
|
+
if node.func.id == "_integer":
|
|
485
|
+
return {"type": "integer", "default": arguments[0], "minimum": arguments[1], "maximum": arguments[2]}
|
|
486
|
+
if node.func.id == "_reporting":
|
|
487
|
+
return {"type": "object", "properties": {"format": {"type": "string", "enum": ["markdown", "json"], "default": "markdown"}}, "required": [], "additionalProperties": False, "default": {}}
|
|
488
|
+
if node.func.id == "_copy":
|
|
489
|
+
return copy.deepcopy(arguments[0])
|
|
490
|
+
raise DocumentationError(f"schema expression is outside the metadata allowlist: {type(node).__name__}")
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def extract_parameter_schemas(root: Path) -> dict[str, dict]:
|
|
494
|
+
schemas = {}
|
|
495
|
+
for module, function in (("evals", "default_evaluation_registry"), ("public_eval", "public_definition")):
|
|
496
|
+
tree = ast.parse((root / f"src/sphynx/{module}.py").read_text(encoding="utf-8-sig"))
|
|
497
|
+
names = {node.targets[0].id: node.value.value for node in tree.body if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name) and isinstance(node.value, ast.Constant)}
|
|
498
|
+
body = _find(tree, function).body
|
|
499
|
+
for node in body:
|
|
500
|
+
if not isinstance(node, ast.Assign) or len(node.targets) != 1:
|
|
501
|
+
continue
|
|
502
|
+
target = node.targets[0]
|
|
503
|
+
if isinstance(target, ast.Name) and target.id in {"bank", "legacy", "configurable", "props", "schema"}:
|
|
504
|
+
names[target.id] = _metadata_expression(node.value, names)
|
|
505
|
+
elif isinstance(target, ast.Subscript) and isinstance(target.value, ast.Name) and target.value.id == "props":
|
|
506
|
+
names["props"][_metadata_expression(target.slice, names)] = _metadata_expression(node.value, names)
|
|
507
|
+
if module == "evals":
|
|
508
|
+
schemas["rag.rerank.v1"] = names["legacy"]
|
|
509
|
+
schemas["rag.rerank.v2"] = names["configurable"]
|
|
510
|
+
for node in ast.walk(tree):
|
|
511
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "EvaluationDefinition" and isinstance(node.args[0], ast.Constant) and node.args[0].value == "audio.similarity.v1":
|
|
512
|
+
schemas["audio.similarity.v1"] = _metadata_expression(node.args[6], names)
|
|
513
|
+
else:
|
|
514
|
+
schemas["rag.rerank.public.v1"] = names["schema"]
|
|
515
|
+
return schemas
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def flatten_parameter_schema(schema: dict) -> list[dict]:
|
|
519
|
+
rows = {}
|
|
520
|
+
def visit(node, prefix="", required=False, inherited=None):
|
|
521
|
+
default = node.get("default", inherited)
|
|
522
|
+
if "oneOf" in node:
|
|
523
|
+
for branch in node["oneOf"]:
|
|
524
|
+
visit(branch, prefix, required, default)
|
|
525
|
+
elif node.get("type") == "object":
|
|
526
|
+
for name, child in node["properties"].items():
|
|
527
|
+
child_default = default.get(name) if isinstance(default, dict) else None
|
|
528
|
+
visit(child, f"{prefix}.{name}" if prefix else name, name in node.get("required", []), child_default)
|
|
529
|
+
else:
|
|
530
|
+
row = {"name": prefix, "type": node.get("type", "union"), "required": required and default is None, "default": default, "accepted": [str(value).lower() if type(value) is bool else str(value) for value in node.get("enum", [])]}
|
|
531
|
+
if prefix in rows:
|
|
532
|
+
row["accepted"] = sorted(set(rows[prefix]["accepted"]) | set(row["accepted"]))
|
|
533
|
+
row["default"] = rows[prefix]["default"] if rows[prefix]["default"] is not None else row["default"]
|
|
534
|
+
row["required"] = rows[prefix]["required"] and row["required"]
|
|
535
|
+
rows[prefix] = row
|
|
536
|
+
visit(schema)
|
|
537
|
+
return list(rows.values())
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def snapshot_contract(root: Path, *, commit: str, tree: str, wheel_sha256: str) -> dict:
|
|
541
|
+
"""Create a reviewable source lock from an explicitly reviewed pinned source."""
|
|
542
|
+
_hash(commit, "source commit", 40)
|
|
543
|
+
_hash(tree, "source tree", 40)
|
|
544
|
+
_hash(wheel_sha256, "wheel digest")
|
|
545
|
+
version = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8-sig"))["project"]["version"]
|
|
546
|
+
interfaces = extract_api(root, commit)
|
|
547
|
+
# Python/TOML source is interpreted as UTF-8 text. Normalize a UTF-8 BOM
|
|
548
|
+
# and universal line endings only, so Git checkout EOL settings do not
|
|
549
|
+
# change receipts for semantically identical reviewed source text.
|
|
550
|
+
receipts = {path: sha256((root / path).read_text(encoding="utf-8-sig").replace("\r\n", "\n").replace("\r", "\n").encode("utf-8")) for path in SOURCE_FILES}
|
|
551
|
+
return {"schema_version": "sphynx.docs.source-contract.v1", "sdk_version": version, "source_commit": commit, "source_tree": tree, "wheel_sha256": wheel_sha256, "source_receipts": receipts, "source_receipts_format": SOURCE_RECEIPTS_FORMAT, "signatures": {item["id"]: item["signature"] for item in interfaces}, "definition_statuses": _definition_statuses(root), "parameter_schemas": extract_parameter_schemas(root)}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def build_payload(root: Path, *, release_id: str | None = None) -> tuple[dict, dict, dict]:
|
|
555
|
+
public = root / "docs/public"
|
|
556
|
+
lock = _read_json(public / "source-contract.v1.json")
|
|
557
|
+
current = snapshot_contract(root, commit=lock["source_commit"], tree=lock["source_tree"], wheel_sha256=lock["wheel_sha256"])
|
|
558
|
+
if current != lock:
|
|
559
|
+
changed = [key for key in current if current[key] != lock.get(key)]
|
|
560
|
+
raise DocumentationError(f"stale documentation/source lock: changed {', '.join(changed)}; review and refresh source contract")
|
|
561
|
+
content = _read_json(public / "content.v1.json")
|
|
562
|
+
catalog = _read_json(public / "catalog.v1.json")
|
|
563
|
+
release = _read_json(public / "release.v1.json")
|
|
564
|
+
_keys(release, "release")
|
|
565
|
+
if release["exporter_version"] != EXPORTER_VERSION:
|
|
566
|
+
raise DocumentationError("curated release must select the current exporter version")
|
|
567
|
+
for field in ("sdk_version", "source_commit", "source_tree", "wheel_sha256"):
|
|
568
|
+
if release[field] != lock[field]:
|
|
569
|
+
raise DocumentationError(f"release/source contract identity mismatch: {field}")
|
|
570
|
+
if release_id is not None:
|
|
571
|
+
release["id"] = release_id
|
|
572
|
+
if set(content) != {"pages", "suites", "evidence", "versions", "known_gaps", "api_descriptions"} or set(catalog) != {"families", "methods", "datasets", "controls"}:
|
|
573
|
+
raise DocumentationError("unapproved curated source keys")
|
|
574
|
+
api = extract_api(root, lock["source_commit"], content["api_descriptions"])
|
|
575
|
+
interface_map = {item["id"]: item for item in api}
|
|
576
|
+
if set(content["api_descriptions"]) != set(interface_map):
|
|
577
|
+
raise DocumentationError("missing or stale public API documentation descriptions")
|
|
578
|
+
for identity, description in content["api_descriptions"].items():
|
|
579
|
+
if identity not in interface_map or set(description) - {"summary", "parameters"}:
|
|
580
|
+
raise DocumentationError(f"unknown or unapproved API description: {identity}")
|
|
581
|
+
names = {parameter["name"] for parameter in interface_map[identity]["parameters"]}
|
|
582
|
+
if set(description.get("parameters", {})) != names:
|
|
583
|
+
raise DocumentationError(f"stale parameter descriptions: {identity}")
|
|
584
|
+
for explanation in description.get("parameters", {}).values():
|
|
585
|
+
if not isinstance(explanation, dict) or set(explanation) != {"description", "accepted"}:
|
|
586
|
+
raise DocumentationError(f"invalid API parameter explanation: {identity}")
|
|
587
|
+
payload = {
|
|
588
|
+
"schema_version": SCHEMA,
|
|
589
|
+
"release": release,
|
|
590
|
+
**{key: value for key, value in content.items() if key != "api_descriptions"}, **catalog, "api": api,
|
|
591
|
+
"reconciliation": [
|
|
592
|
+
{"discovery": "list_packs", "suite_ids": ["rag.rerank.v1", "rag.rerank.sable.dev.v1", "rag.rerank.beir.v1", "rag.invoke.v1", "rag.retrieve.v1", "rag.question_robustness.v1"], "missing_suite_ids": ["rag.chunk.v1", "rag.pipeline.v1", "rag.full_rag.v1", "rag.rerank.public.v1"], "explanation": "Six local pack descriptors are not exhaustive SDK capability discovery. The hosted public definition and separately implemented DatasetV3 APIs are reconciled here."},
|
|
593
|
+
{"discovery": "list_evals", "suite_ids": ["rag.rerank.v1", "rag.rerank.public.v1", "rag.rerank.v2", "audio.similarity.v1"], "missing_suite_ids": sorted(IMPLEMENTED_IDS - HOSTED_IDS), "explanation": "Two runnable definitions and two contract-only definitions do not enumerate local evaluation APIs."},
|
|
594
|
+
{"discovery": "list_datasets", "suite_ids": [], "missing_suite_ids": [], "explanation": "34 dataset descriptors describe metadata and loader readiness, not 34 evaluations or private-data approval."},
|
|
595
|
+
],
|
|
596
|
+
}
|
|
597
|
+
reference_source = public / "reference.v1.json"
|
|
598
|
+
if reference_source.exists():
|
|
599
|
+
payload["references"] = _read_json(reference_source)
|
|
600
|
+
statuses = lock["definition_statuses"]
|
|
601
|
+
for suite in payload["suites"]:
|
|
602
|
+
if suite["id"] in statuses and ("runnable" if suite["status"] == "implemented" else "contract_only") != statuses[suite["id"]]:
|
|
603
|
+
raise DocumentationError(f"stale suite status: {suite['id']}")
|
|
604
|
+
if suite["id"] in lock["parameter_schemas"]:
|
|
605
|
+
documented = {parameter["name"]: parameter for parameter in suite["parameters"]}
|
|
606
|
+
for parameter in flatten_parameter_schema(lock["parameter_schemas"][suite["id"]]):
|
|
607
|
+
actual = documented.get(parameter["name"])
|
|
608
|
+
if actual is None:
|
|
609
|
+
raise DocumentationError(f"missing suite parameter documentation: {suite['id']}.{parameter['name']}")
|
|
610
|
+
if actual["default"] != parameter["default"] or actual["required"] != parameter["required"] or not set(parameter["accepted"]) <= set(actual["accepted"]):
|
|
611
|
+
raise DocumentationError(f"stale suite parameter default/required/options: {suite['id']}.{parameter['name']}")
|
|
612
|
+
validate_payload(payload)
|
|
613
|
+
return payload, catalog, lock
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def export_bundle(source_root: str | Path, output_dir: str | Path, *, release_id: str | None = None, build_time_utc: str | None = None) -> dict:
|
|
617
|
+
"""Write deterministic public JSON and an independently timestamped manifest."""
|
|
618
|
+
root, output = Path(source_root), Path(output_dir)
|
|
619
|
+
payload, catalog, lock = build_payload(root, release_id=release_id)
|
|
620
|
+
raw = canonical_bytes(payload)
|
|
621
|
+
catalog_raw = canonical_bytes(catalog)
|
|
622
|
+
manifest = {"schema_version": MANIFEST_SCHEMA, "content_sha256": sha256(raw), "content_bytes": len(raw), "catalog_sha256": sha256(catalog_raw), "source_receipts": lock["source_receipts"], "source_receipts_format": SOURCE_RECEIPTS_FORMAT, "source_receipts_sha256": sha256(canonical_bytes(lock["source_receipts"])), "build_time_utc": build_time_utc or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), "exporter_sha256": sha256(Path(__file__).read_bytes())}
|
|
623
|
+
if "references" in payload:
|
|
624
|
+
manifest["references_sha256"] = sha256(canonical_bytes(payload["references"]))
|
|
625
|
+
curated_files = ["catalog.v1.json", "content.v1.json", "release.v1.json", "reference.v1.json"]
|
|
626
|
+
documentation_receipts = {f"docs/public/{name}": sha256(canonical_bytes(_read_json(root / "docs/public" / name))) for name in curated_files}
|
|
627
|
+
manifest["documentation_source_receipts"] = documentation_receipts
|
|
628
|
+
manifest["documentation_source_receipts_sha256"] = sha256(canonical_bytes(documentation_receipts))
|
|
629
|
+
files = (("payload.json", raw), ("catalog.json", catalog_raw), ("manifest.json", canonical_bytes(manifest)))
|
|
630
|
+
for name, data in files:
|
|
631
|
+
destination = output / name
|
|
632
|
+
if destination.exists() and name != "manifest.json" and destination.read_bytes() != data:
|
|
633
|
+
raise DocumentationError("immutable release path contains different content; use a new release ID/output directory")
|
|
634
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
635
|
+
for name, data in files:
|
|
636
|
+
destination = output / name
|
|
637
|
+
destination.write_bytes(data)
|
|
638
|
+
return manifest
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def verify_bundle(bundle_dir: str | Path, *, expected_sha256: str) -> dict:
|
|
642
|
+
"""Reject malformed/unapproved artifacts before an Evals Lab import."""
|
|
643
|
+
_hash(expected_sha256, "approved content digest")
|
|
644
|
+
root = Path(bundle_dir)
|
|
645
|
+
raw = (root / "payload.json").read_bytes()
|
|
646
|
+
if len(raw) > MAX_BUNDLE_BYTES:
|
|
647
|
+
raise DocumentationError("documentation payload exceeds size limit")
|
|
648
|
+
manifest = _read_json(root / "manifest.json")
|
|
649
|
+
_keys(manifest, "manifest", optional=("references_sha256", "documentation_source_receipts", "documentation_source_receipts_sha256"))
|
|
650
|
+
if manifest["source_receipts_format"] != SOURCE_RECEIPTS_FORMAT:
|
|
651
|
+
raise DocumentationError("unsupported source receipt normalization")
|
|
652
|
+
if type(manifest["content_bytes"]) is not int or manifest["schema_version"] != MANIFEST_SCHEMA or manifest["content_sha256"] != expected_sha256 or sha256(raw) != expected_sha256 or manifest["content_bytes"] != len(raw):
|
|
653
|
+
raise DocumentationError("documentation content digest/size mismatch")
|
|
654
|
+
for key in ("catalog_sha256", "source_receipts_sha256", "exporter_sha256"):
|
|
655
|
+
_hash(manifest[key], key)
|
|
656
|
+
receipts = manifest["source_receipts"]
|
|
657
|
+
if set(receipts) not in (set(SOURCE_FILES), set(PREVIOUS_SOURCE_FILES), set(LEGACY_SOURCE_FILES)) or any(not re.fullmatch(r"(?:pyproject\.toml|src/sphynx/(?:rag_papers/)?[a-z_]+\.py)", path) for path in receipts):
|
|
658
|
+
raise DocumentationError("unapproved source receipt paths")
|
|
659
|
+
for value in receipts.values():
|
|
660
|
+
_hash(value, "source receipt")
|
|
661
|
+
if sha256(canonical_bytes(receipts)) != manifest["source_receipts_sha256"]:
|
|
662
|
+
raise DocumentationError("source receipt digest mismatch")
|
|
663
|
+
payload = _read_json(root / "payload.json")
|
|
664
|
+
catalog = _read_json(root / "catalog.json")
|
|
665
|
+
if sha256((root / "catalog.json").read_bytes()) != manifest["catalog_sha256"] or catalog != {key: payload[key] for key in ("families", "methods", "datasets", "controls")}:
|
|
666
|
+
raise DocumentationError("catalogue digest/projection mismatch")
|
|
667
|
+
validate_payload(payload)
|
|
668
|
+
reference_receipt_fields = {"references_sha256", "documentation_source_receipts", "documentation_source_receipts_sha256"}
|
|
669
|
+
if set(manifest) & reference_receipt_fields and not reference_receipt_fields <= set(manifest):
|
|
670
|
+
raise DocumentationError("reference source receipts must be supplied together")
|
|
671
|
+
if "references" in payload:
|
|
672
|
+
if "references_sha256" not in manifest or sha256(canonical_bytes(payload["references"])) != manifest["references_sha256"]:
|
|
673
|
+
raise DocumentationError("reference digest mismatch")
|
|
674
|
+
elif "references_sha256" in manifest:
|
|
675
|
+
raise DocumentationError("reference digest present without reference content")
|
|
676
|
+
has_documentation_receipts = "documentation_source_receipts" in manifest
|
|
677
|
+
if has_documentation_receipts != ("documentation_source_receipts_sha256" in manifest):
|
|
678
|
+
raise DocumentationError("documentation source receipts require their digest")
|
|
679
|
+
if has_documentation_receipts:
|
|
680
|
+
documentation_receipts = manifest["documentation_source_receipts"]
|
|
681
|
+
expected_paths = {"docs/public/catalog.v1.json", "docs/public/content.v1.json", "docs/public/release.v1.json"}
|
|
682
|
+
if "references" in payload:
|
|
683
|
+
expected_paths.add("docs/public/reference.v1.json")
|
|
684
|
+
if not isinstance(documentation_receipts, dict) or set(documentation_receipts) != expected_paths:
|
|
685
|
+
raise DocumentationError("unapproved documentation receipt paths")
|
|
686
|
+
for value in documentation_receipts.values():
|
|
687
|
+
_hash(value, "documentation source receipt")
|
|
688
|
+
if sha256(canonical_bytes(documentation_receipts)) != manifest["documentation_source_receipts_sha256"]:
|
|
689
|
+
raise DocumentationError("documentation source receipt digest mismatch")
|
|
690
|
+
if "references" in payload and documentation_receipts["docs/public/reference.v1.json"] != manifest["references_sha256"]:
|
|
691
|
+
raise DocumentationError("reference source receipt mismatch")
|
|
692
|
+
return {"manifest": manifest, "payload": payload}
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def main(argv: list[str] | None = None) -> int:
|
|
696
|
+
parser = argparse.ArgumentParser(description="Sphynx metadata-only documentation exporter (implemented v1)")
|
|
697
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
698
|
+
default_root = Path(__file__).resolve().parents[2]
|
|
699
|
+
for name in ("snapshot-contract", "check", "export"):
|
|
700
|
+
command = sub.add_parser(name)
|
|
701
|
+
command.add_argument("--source-root", type=Path, default=default_root)
|
|
702
|
+
if name == "snapshot-contract":
|
|
703
|
+
command.add_argument("--source-commit", required=True)
|
|
704
|
+
command.add_argument("--source-tree", required=True)
|
|
705
|
+
command.add_argument("--wheel-sha256", required=True)
|
|
706
|
+
if name == "export":
|
|
707
|
+
command.add_argument("--output", type=Path, required=True)
|
|
708
|
+
command.add_argument("--release-id")
|
|
709
|
+
command.add_argument("--build-time-utc")
|
|
710
|
+
verify = sub.add_parser("verify")
|
|
711
|
+
verify.add_argument("--bundle-dir", type=Path, required=True)
|
|
712
|
+
verify.add_argument("--expected-sha256", required=True)
|
|
713
|
+
args = parser.parse_args(argv)
|
|
714
|
+
try:
|
|
715
|
+
if args.command == "snapshot-contract":
|
|
716
|
+
contract = snapshot_contract(args.source_root, commit=args.source_commit, tree=args.source_tree, wheel_sha256=args.wheel_sha256)
|
|
717
|
+
path = args.source_root / "docs/public/source-contract.v1.json"
|
|
718
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
719
|
+
path.write_bytes(canonical_bytes(contract))
|
|
720
|
+
print(f"Review source contract: {path}")
|
|
721
|
+
elif args.command == "check":
|
|
722
|
+
payload, _, _ = build_payload(args.source_root)
|
|
723
|
+
print(f"Documentation check passed: {len(payload['suites'])} suites, {len(payload['methods'])} methods, {len(payload['api'])} API entries; no evaluation executed")
|
|
724
|
+
elif args.command == "export":
|
|
725
|
+
manifest = export_bundle(args.source_root, args.output, release_id=args.release_id, build_time_utc=args.build_time_utc)
|
|
726
|
+
print(json.dumps({"output": str(args.output), "content_sha256": manifest["content_sha256"], "content_bytes": manifest["content_bytes"]}))
|
|
727
|
+
else:
|
|
728
|
+
result = verify_bundle(args.bundle_dir, expected_sha256=args.expected_sha256)
|
|
729
|
+
print(f"Verified approved documentation release: {result['payload']['release']['id']}")
|
|
730
|
+
return 0
|
|
731
|
+
except (DocumentationError, OSError, KeyError, TypeError, SyntaxError) as error:
|
|
732
|
+
print(f"Documentation error: {error}", file=sys.stderr)
|
|
733
|
+
return 1
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
if __name__ == "__main__":
|
|
737
|
+
raise SystemExit(main())
|