"""Compose task profiles from Markdown capability cards.""" from __future__ import annotations import argparse import json from pathlib import Path from typing import Any REGISTRY_ROOT = Path(__file__).resolve().parents[1] / "docs" / "registry" def parse_scalar(value: str) -> Any: value = value.strip() if value in {"true", "True"}: return True if value in {"false", "False"}: return False if value in {"null", "None"}: return None if value.startswith("[") and value.endswith("]"): inner = value[1:-1].strip() if not inner: return [] return [parse_scalar(part.strip()) for part in inner.split(",")] try: if "." in value: return float(value) return int(value) except ValueError: return value.strip("\"'") def parse_front_matter(text: str) -> tuple[dict[str, Any], str]: lines = text.splitlines() if not lines or lines[0].strip() != "---": return {}, text meta: dict[str, Any] = {} end = None for idx, line in enumerate(lines[1:], start=1): if line.strip() == "---": end = idx break if not line.strip() or line.lstrip().startswith("#"): continue if ":" not in line: continue key, value = line.split(":", 1) meta[key.strip()] = parse_scalar(value) if end is None: return meta, text return meta, "\n".join(lines[end + 1 :]).strip() def load_card(kind: str, card_id: str, registry_root: Path) -> dict[str, Any]: path = registry_root / kind / f"{card_id}.md" if not path.exists(): available = sorted(p.stem for p in (registry_root / kind).glob("*.md")) raise FileNotFoundError(f"Card not found: {path}. Available {kind}: {available}") text = path.read_text(encoding="utf-8") meta, body = parse_front_matter(text) return {"kind": kind, "id": card_id, "path": str(path), "meta": meta, "body": body} def list_cards(registry_root: Path) -> dict[str, list[str]]: result: dict[str, list[str]] = {} for kind in ("elements", "satellites", "sensors", "resolutions"): folder = registry_root / kind result[kind] = sorted(p.stem for p in folder.glob("*.md")) if folder.exists() else [] return result def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--element") parser.add_argument("--satellite") parser.add_argument("--sensor") parser.add_argument("--resolution") parser.add_argument( "--fusion", help="Optional sensor/product fusion card, for example FUSED_OPTICAL or STREAM_FUSION.", ) parser.add_argument("--registry-root", default=str(REGISTRY_ROOT)) parser.add_argument("--output", required=False) parser.add_argument("--list", action="store_true", help="List available cards and exit.") return parser.parse_args() def main() -> None: args = parse_args() registry_root = Path(args.registry_root) if args.list: print(json.dumps(list_cards(registry_root), indent=2, ensure_ascii=False)) return required = { "elements": args.element, "satellites": args.satellite, "sensors": args.sensor, "resolutions": args.resolution, } missing = [key for key, value in required.items() if not value] if missing: raise SystemExit(f"Missing required cards: {missing}. Use --list to inspect available cards.") cards = {kind: load_card(kind, card_id, registry_root) for kind, card_id in required.items() if card_id} if args.fusion: cards["fusion"] = load_card("sensors", args.fusion, registry_root) element_meta = cards["elements"]["meta"] sensor_meta = cards["sensors"]["meta"] resolution_meta = cards["resolutions"]["meta"] fusion_meta = cards.get("fusion", {}).get("meta", {}) fusion_state = fusion_meta.get("fusion_state", sensor_meta.get("fusion_state", "none")) profile = { "profile_id": "_".join( part for part in [args.element, args.satellite, args.sensor, args.fusion, args.resolution] if part ), "cards": cards, "task": { "element": args.element, "task_types": element_meta.get("task_types", []), "preferred_heads": element_meta.get("preferred_heads", []), "label_formats": element_meta.get("label_formats", []), "negative_policy": element_meta.get("negative_policy", "unlabeled_is_ignore"), }, "input": { "satellite": args.satellite, "sensor": args.sensor, "resolution_m": resolution_meta.get("resolution_m"), "modalities": sensor_meta.get("modalities", []), "common_bands": sensor_meta.get("common_bands", []), "recommended_patch_sizes": resolution_meta.get("recommended_patch_sizes", []), }, "fusion": { "card": args.fusion, "state": fusion_state, "modalities": fusion_meta.get("modalities", sensor_meta.get("modalities", [])), "supports_streaming_fusion": fusion_meta.get( "supports_streaming_fusion", sensor_meta.get("supports_streaming_fusion", False) ), "requires_fusion_metadata": fusion_meta.get( "requires_fusion_metadata", sensor_meta.get("requires_fusion_metadata", False) ), "required_manifest_fields": [ "state", "method", "sources", "target_resolution_m", "native_multispectral_resolution_m", "persisted", "reproducible", "spectral_preservation", ], }, "constraints": { "do_not_assume_external_coastline_or_land_mask": True, "unlabeled_elements_are_ignore_not_negative": True, }, } output = json.dumps(profile, indent=2, ensure_ascii=False) if args.output: output_path = Path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(output + "\n", encoding="utf-8") print(output_path) else: print(output) if __name__ == "__main__": main()