From 289ca51b22bfe77435e0d4b8113731246dffc5b4 Mon Sep 17 00:00:00 2001 From: OpenClaw Auto Earner Date: Thu, 13 Aug 2026 19:43:11 +0000 Subject: [PATCH] Add app SDK schema and TypeScript contracts --- README.md | 27 ++++ schemas/plantguide-care-report.schema.json | 179 +++++++++++++++++++++ schemas/plantguide-sdk.d.ts | 58 +++++++ tests/test_sdk_contracts.py | 61 +++++++ 4 files changed, 325 insertions(+) create mode 100644 schemas/plantguide-care-report.schema.json create mode 100644 schemas/plantguide-sdk.d.ts create mode 100644 tests/test_sdk_contracts.py diff --git a/README.md b/README.md index b6e3281..6a2c82f 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - [CLI reference](#cli-reference) - [How photo ID works](#how-photo-id-works) - [App care report](#app-care-report) +- [App SDK contracts](#app-sdk-contracts) - [Species catalog](#species-catalog) - [Diagrams](#diagrams) - [Repository layout](#repository-layout) @@ -192,6 +193,32 @@ plantguide demo photo -i data/samples/photos/monstera_demo.jpg --out data/out/ph Reports include `query_tags` / image path, ranked `matches`, `care` card, and (for photo demo) `watering` + optional `care_svg`. +## App SDK contracts + +PlantGuide publishes stable app-integration contracts under `schemas/`: + +| File | Purpose | +| --- | --- | +| `schemas/plantguide-care-report.schema.json` | JSON Schema for `plantguide.app.care_report.v1` payloads emitted by `plantguide.integrations.sdk` | +| `schemas/plantguide-sdk.d.ts` | TypeScript interfaces for app clients consuming care reports and care cards | + +The schema is checked against real Python SDK payloads in CI. + +```powershell +plantguide app demo --sample data/samples/obs_monstera.json --out data/out/e2e-monstera-care-report.json +``` + +TypeScript clients can vendor or import the declarations: + +```ts +import type { PlantGuideCareReport } from "./schemas/plantguide-sdk"; + +function renderCare(report: PlantGuideCareReport) { + if (!report.ready_for_ui || !report.care_card) return; + console.log(report.care_card.common_name, report.care_card.water); +} +``` + --- ## Species catalog diff --git a/schemas/plantguide-care-report.schema.json b/schemas/plantguide-care-report.schema.json new file mode 100644 index 0000000..76141d9 --- /dev/null +++ b/schemas/plantguide-care-report.schema.json @@ -0,0 +1,179 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mergeos-bounties.github.io/PlantGuide/schemas/plantguide-care-report.schema.json", + "title": "PlantGuide app care report", + "description": "Stable JSON contract emitted by plantguide.integrations.sdk care report helpers.", + "type": "object", + "additionalProperties": false, + "required": [ + "report_type", + "integration_version", + "ready_for_ui", + "sample_id", + "source", + "query_tags", + "expected_species", + "hit_top1", + "top_species_id", + "top_match", + "matches", + "care_card", + "license_safe_evidence" + ], + "properties": { + "report_type": { + "const": "plantguide.app.care_report.v1" + }, + "integration_version": { + "const": "plantguide.sdk.v1" + }, + "ready_for_ui": { + "type": "boolean" + }, + "sample_id": { + "type": ["string", "null"] + }, + "source": { + "type": ["string", "null"] + }, + "query_tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "expected_species": { + "type": ["string", "null"] + }, + "hit_top1": { + "type": ["boolean", "null"] + }, + "top_species_id": { + "type": ["string", "null"] + }, + "top_match": { + "$ref": "#/$defs/match" + }, + "matches": { + "type": "array", + "items": { + "$ref": "#/$defs/match" + } + }, + "care_card": { + "$ref": "#/$defs/care_card" + }, + "license_safe_evidence": { + "type": "object", + "additionalProperties": false, + "required": ["source", "source_type", "external_assets"], + "properties": { + "source": { + "type": "string" + }, + "source_type": { + "enum": ["sample_fixture", "inline_tags"] + }, + "external_assets": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "$defs": { + "match": { + "type": ["object", "null"], + "required": ["species_id", "common_name", "scientific_name", "score", "matched_tags"], + "properties": { + "species_id": { + "type": "string" + }, + "common_name": { + "type": "string" + }, + "scientific_name": { + "type": "string" + }, + "score": { + "type": "number" + }, + "matched_tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": true + }, + "care_card": { + "type": ["object", "null"], + "required": [ + "species_id", + "common_name", + "scientific_name", + "summary", + "light", + "water", + "soil", + "humidity", + "temperature_c", + "fertilizer", + "toxicity", + "common_issues", + "tips" + ], + "properties": { + "species_id": { + "type": "string" + }, + "common_name": { + "type": "string" + }, + "scientific_name": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "light": { + "type": "string" + }, + "water": { + "type": "string" + }, + "soil": { + "type": "string" + }, + "humidity": { + "type": "string" + }, + "temperature_c": { + "type": "string" + }, + "fertilizer": { + "type": "string" + }, + "toxicity": { + "type": "string" + }, + "common_issues": { + "type": "array", + "items": { + "type": "string" + } + }, + "tips": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": true + } + } +} diff --git a/schemas/plantguide-sdk.d.ts b/schemas/plantguide-sdk.d.ts new file mode 100644 index 0000000..b09d30a --- /dev/null +++ b/schemas/plantguide-sdk.d.ts @@ -0,0 +1,58 @@ +export interface PlantGuideMatch { + species_id: string; + common_name: string; + scientific_name: string; + score: number; + matched_tags: string[]; + [key: string]: unknown; +} + +export interface PlantGuideCareCard { + species_id: string; + common_name: string; + scientific_name: string; + summary: string; + light: string; + water: string; + soil: string; + humidity: string; + temperature_c: string; + fertilizer: string; + toxicity: string; + common_issues: string[]; + tips: string[]; + [key: string]: unknown; +} + +export interface PlantGuideLicenseSafeEvidence { + source: string; + source_type: "sample_fixture" | "inline_tags"; + external_assets: string[]; +} + +export interface PlantGuideCareReport { + report_type: "plantguide.app.care_report.v1"; + integration_version: "plantguide.sdk.v1"; + ready_for_ui: boolean; + sample_id: string | null; + source: string | null; + query_tags: string[]; + expected_species: string | null; + hit_top1: boolean | null; + top_species_id: string | null; + top_match: PlantGuideMatch | null; + matches: PlantGuideMatch[]; + care_card: PlantGuideCareCard | null; + license_safe_evidence: PlantGuideLicenseSafeEvidence; +} + +export declare function assessForApp(tags: string | string[], topK?: number): Promise; +export declare function careForApp(speciesId: string): Promise; +export declare function careReportFromSample( + samplePath: string, + topK?: number, +): Promise; +export declare function careReportFromTags( + tags: string | string[], + topK?: number, +): Promise; diff --git a/tests/test_sdk_contracts.py b/tests/test_sdk_contracts.py new file mode 100644 index 0000000..181d711 --- /dev/null +++ b/tests/test_sdk_contracts.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from plantguide.integrations.sdk import care_report_from_sample, care_report_from_tags + +SCHEMA_PATH = Path("schemas/plantguide-care-report.schema.json") +TYPES_PATH = Path("schemas/plantguide-sdk.d.ts") + + +def test_care_report_schema_matches_sample_payload_contract() -> None: + schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) + report = care_report_from_sample(Path("data/samples/obs_monstera.json"), top_k=2) + + assert set(schema["required"]) == set(report) + assert schema["properties"]["report_type"]["const"] == report["report_type"] + assert schema["properties"]["integration_version"]["const"] == report["integration_version"] + assert schema["properties"]["license_safe_evidence"]["properties"]["source_type"]["enum"] + _assert_required_keys( + schema["$defs"]["care_card"]["required"], + report["care_card"], + ) + _assert_required_keys( + schema["$defs"]["match"]["required"], + report["top_match"], + ) + + +def test_care_report_schema_matches_inline_tags_payload_contract() -> None: + schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) + report = care_report_from_tags("succulent,thick leaves,drought", top_k=1) + + assert set(schema["required"]) == set(report) + assert report["sample_id"] is None + assert report["license_safe_evidence"]["source_type"] == "inline_tags" + _assert_required_keys( + schema["$defs"]["care_card"]["required"], + report["care_card"], + ) + _assert_required_keys( + schema["$defs"]["match"]["required"], + report["top_match"], + ) + + +def test_typescript_contract_exports_match_schema_names() -> None: + types = TYPES_PATH.read_text(encoding="utf-8") + + assert "export interface PlantGuideCareReport" in types + assert "report_type: \"plantguide.app.care_report.v1\"" in types + assert "integration_version: \"plantguide.sdk.v1\"" in types + assert "source_type: \"sample_fixture\" | \"inline_tags\"" in types + assert "export interface PlantGuideCareCard" in types + + +def _assert_required_keys(required: list[str], payload: Any) -> None: + assert isinstance(payload, dict) + missing = set(required) - set(payload) + assert not missing