From ee893bff1690c49f7bc24776e935f95c1f2008d6 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:23:01 +0530 Subject: [PATCH] fix: version workspace status JSON contract --- cli/python/base_projects/tests/test_engine.py | 2 + .../base_projects/workspace_report_json.py | 1 + docs/command-reference.md | 2 +- docs/contracts.md | 1 + docs/output-formats.md | 6 +- docs/schemas/workspace-status.json | 140 ++++++++++++++++++ docs/stability-tiers.md | 2 + tests/contracts/run.sh | 1 + .../contracts/test_workspace_status_schema.py | 71 +++++++++ tests/test_contract_hardening.py | 3 + tests/test_stability_tiers_docs.py | 16 +- 11 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 docs/schemas/workspace-status.json create mode 100644 tests/contracts/test_workspace_status_schema.py diff --git a/cli/python/base_projects/tests/test_engine.py b/cli/python/base_projects/tests/test_engine.py index 976fd680..d8993b29 100644 --- a/cli/python/base_projects/tests/test_engine.py +++ b/cli/python/base_projects/tests/test_engine.py @@ -595,6 +595,7 @@ def test_workspace_status_supports_json_format(self) -> None: self.assertEqual(status, 0) self.assertEqual(stderr, "") self.assertTrue(stdout.startswith("{\n")) + self.assertEqual(payload["schema_version"], 1) self.assertIn(' "workspace": ', stdout) self.assertEqual(payload["status"], "warn") self.assertEqual(payload["workspace"], str(workspace.resolve())) @@ -710,6 +711,7 @@ def test_workspace_status_json_aggregates_mixed_project_states(self) -> None: projects_by_name = {project["name"]: project for project in payload["projects"]} self.assertEqual(status, 1) self.assertEqual(stderr, "") + self.assertEqual(payload["schema_version"], 1) self.assertEqual(payload["status"], "error") self.assertEqual(projects_by_name["healthy"]["status"], "ok") self.assertEqual(projects_by_name["attention"]["status"], "warn") diff --git a/cli/python/base_projects/workspace_report_json.py b/cli/python/base_projects/workspace_report_json.py index 32168333..8d410695 100644 --- a/cli/python/base_projects/workspace_report_json.py +++ b/cli/python/base_projects/workspace_report_json.py @@ -26,6 +26,7 @@ def workspace_status_to_json( workspace_manifest: WorkspaceManifest | None = None, ) -> dict[str, Any]: payload: dict[str, Any] = { + "schema_version": 1, "workspace": str(workspace_root), "status": most_severe_status(*(status.status for status in statuses)), "project_count": workspace_project_count(statuses, workspace_manifest), diff --git a/docs/command-reference.md b/docs/command-reference.md index 9b3ca123..0a616d7c 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -204,7 +204,7 @@ manifest trust. | Command | What it does | Important flags | |---|---|---| -| `basectl workspace status` | Show read-only workspace project status and latest recorded project check dates. Uses `workspace.manifest` from user config unless `--manifest` is supplied. | `--workspace `, `--manifest `, `--format ` | +| `basectl workspace status` | Show read-only workspace project status and latest recorded project check dates. Uses `workspace.manifest` from user config unless `--manifest` is supplied. JSON uses the stable schema [`docs/schemas/workspace-status.json`](schemas/workspace-status.json). | `--workspace `, `--manifest `, `--format ` | | `basectl workspace check` | Render check-oriented readiness state across workspace projects. Text output summarizes check names and messages; JSON keeps the diagnostic envelope. Uses `workspace.manifest` from user config unless `--manifest` is supplied. | `--workspace `, `--manifest `, `--format `, `--verify-project-runtime` | | `basectl workspace doctor` | Render actionable workspace diagnostics with stable finding IDs and fix guidance. Uses `workspace.manifest` from user config unless `--manifest` is supplied. | `--workspace `, `--manifest `, `--format `, `--verify-project-runtime` | | `basectl workspace onboarding` | Summarize expected-repository first-day state and a prioritized executable next-actions sequence without cloning or setup. Requires a configured or explicit workspace manifest. | `--workspace `, `--manifest `, `--format ` | diff --git a/docs/contracts.md b/docs/contracts.md index d676e0ed..138a8e3c 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -24,6 +24,7 @@ before treating the fix as complete. | Workspace agent-brief JSON schema | [Workspace Manifest](workspace-manifest.md), [`schemas/workspace-agent-brief.json`](schemas/workspace-agent-brief.json) | `tests/contracts/test_workspace_agent_brief_schema.py` | `basectl workspace agent-brief --format json` no longer conforms to its published schema | Workspace | | Workspace update JSON schema | [Workspace Manifest](workspace-manifest.md), [`schemas/workspace-update.json`](schemas/workspace-update.json) | `tests/contracts/test_workspace_update_schema.py` | `basectl workspace update --format json` no longer conforms to its published schema | Workspace | | Workspace test JSON schema | [Workspace Manifest](workspace-manifest.md), [`schemas/workspace-test.json`](schemas/workspace-test.json) | `tests/contracts/test_workspace_test_schema.py` | `basectl workspace test --format json` no longer conforms to its published schema | Workspace | +| Workspace status JSON schema | [Workspace Manifest](workspace-manifest.md), [`schemas/workspace-status.json`](schemas/workspace-status.json) | `tests/contracts/test_workspace_status_schema.py` | `basectl workspace status --format json` no longer conforms to its published schema | Workspace | | Read-only inspection JSON | [Inspection JSON](inspection-json.md), [Command Quick Reference](command-reference.md) | `cli/bash/commands/basectl/tests/inspection-json.bats`, `cli/python/base_release/tests/test_engine.py`, [`base-cli/tests/test_inspection.py`](https://github.com/basefoundry/base-cli/blob/main/tests/test_inspection.py), `tests/test_stability_tiers_docs.py` | A scoped command emits prose or invalid JSON, changes the v1 envelope, loses finding/error semantics, or diverges from text-mode exit policy | CLI | | Project metadata defaults | `.github/base-project.yml`, [GitHub Workflow](github-workflow.md), [Repository Baseline](repo-baseline.md) | `cli/python/base_github_projects/tests/`, `cli/bash/commands/basectl/tests/gh.bats`, `cli/bash/commands/basectl/tests/repo.bats` | Issue defaults, Project field options, or repo-visible Project configuration drift from the Base Project schema | Product | | Canonical positioning documentation | [Product Requirements](product-requirements.md), [Product Assessment](product-assessment.md), [Why Base](why-base.md), and the canonical introduction surfaces | `tests/test_contract_hardening.py` | A canonical newcomer, contributor, or agent surface reintroduces retired product positioning or drops the accepted local-operating-contract thesis and outcome loop | Product | diff --git a/docs/output-formats.md b/docs/output-formats.md index 1cb62b14..136716d3 100644 --- a/docs/output-formats.md +++ b/docs/output-formats.md @@ -32,8 +32,10 @@ basectl workspace status | tee status.tsv # headerless TSV rows basectl workspace status --format json # one stable JSON document ``` -The JSON document emitted by `basectl workspace status --format json` includes -a top-level aggregate `status` alongside `workspace`, `project_count`, and +The JSON document emitted by `basectl workspace status --format json` uses +schema version `1`, published at +[`schemas/workspace-status.json`](schemas/workspace-status.json). It includes a +top-level aggregate `status` alongside `workspace`, `project_count`, and `projects`. The aggregate uses `error` over `warn` over `ok` precedence; each project record retains its existing `status` field. diff --git a/docs/schemas/workspace-status.json b/docs/schemas/workspace-status.json new file mode 100644 index 00000000..0c82a938 --- /dev/null +++ b/docs/schemas/workspace-status.json @@ -0,0 +1,140 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/basefoundry/base/blob/main/docs/schemas/workspace-status.json", + "title": "Base workspace status report", + "description": "Stable schema version 1 for basectl workspace status --format json. Additional properties are allowed for additive, non-breaking fields.", + "type": "object", + "required": [ + "schema_version", + "workspace", + "status", + "project_count", + "projects" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "workspace": { + "type": "string" + }, + "status": { + "enum": ["ok", "warn", "error"] + }, + "project_count": { + "type": "integer", + "minimum": 0 + }, + "projects": { + "type": "array", + "items": { + "$ref": "#/$defs/project" + } + }, + "workspace_manifest": { + "$ref": "workspace-manifest.json" + }, + "repository_count": { + "type": "integer", + "minimum": 0 + } + }, + "$defs": { + "project": { + "type": "object", + "required": [ + "name", + "status", + "path", + "manifest_path", + "venv", + "manifest", + "last_check", + "issues" + ], + "properties": { + "name": { + "type": "string" + }, + "status": { + "enum": ["ok", "warn", "error"] + }, + "path": { + "type": "string" + }, + "manifest_path": { + "type": ["string", "null"] + }, + "venv": { + "type": "string" + }, + "manifest": { + "type": "string" + }, + "last_check": { + "type": ["object", "null"], + "required": ["checked_at", "status"], + "properties": { + "checked_at": { + "type": "string" + }, + "status": { + "enum": ["ok", "warn", "error"] + } + } + }, + "issues": { + "type": "array", + "items": { + "type": "string" + } + }, + "repository": { + "type": "string" + }, + "expected": { + "type": "boolean" + }, + "required": { + "type": "boolean" + }, + "repo": { + "type": "string" + }, + "url": { + "type": "string" + }, + "default_branch": { + "type": "string" + }, + "python_runtime": { + "$ref": "#/$defs/python_runtime" + } + } + }, + "python_runtime": { + "type": "object", + "required": ["manager", "venv", "python", "version"], + "properties": { + "manager": { + "type": "string" + }, + "venv": { + "type": "string" + }, + "python": { + "type": "string" + }, + "version": { + "type": "string" + }, + "requires_python": { + "type": "string" + }, + "pyproject_requires_python": { + "type": "string" + } + } + } + } +} diff --git a/docs/stability-tiers.md b/docs/stability-tiers.md index cbfcd41f..45b8849a 100644 --- a/docs/stability-tiers.md +++ b/docs/stability-tiers.md @@ -80,6 +80,8 @@ Stable JSON contracts include: at [`schemas/workspace-update.json`](schemas/workspace-update.json). - `basectl workspace test --format json`, whose schema version 1 is published at [`schemas/workspace-test.json`](schemas/workspace-test.json). +- `basectl workspace status --format json`, whose schema version 1 is + published at [`schemas/workspace-status.json`](schemas/workspace-status.json). Additive keys are allowed when they do not change the meaning of existing keys. Removing keys, renaming keys, changing value types, or changing enum meanings is diff --git a/tests/contracts/run.sh b/tests/contracts/run.sh index 9d874b34..9990f679 100755 --- a/tests/contracts/run.sh +++ b/tests/contracts/run.sh @@ -46,6 +46,7 @@ run_step "$PYTHON_BIN" -m pytest cli/python/base_projects/tests/test_workspace_a run_step "$PYTHON_BIN" -m pytest tests/contracts/test_workspace_agent_brief_schema.py run_step "$PYTHON_BIN" -m pytest tests/contracts/test_workspace_update_schema.py run_step "$PYTHON_BIN" -m pytest tests/contracts/test_workspace_test_schema.py +run_step "$PYTHON_BIN" -m pytest tests/contracts/test_workspace_status_schema.py run_step "$PYTHON_BIN" -m pytest cli/python/base_release/tests/test_engine.py run_step "$PYTHON_BIN" -m pytest tests/test_stability_tiers_docs.py run_step bats --filter "project installer template" cli/bash/commands/basectl/tests/repo.bats diff --git a/tests/contracts/test_workspace_status_schema.py b/tests/contracts/test_workspace_status_schema.py new file mode 100644 index 00000000..cd45db2f --- /dev/null +++ b/tests/contracts/test_workspace_status_schema.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from tests.contracts.workspace_contract_helpers import run_workspace_command +from tests.contracts.workspace_contract_helpers import validate_workspace_payload + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCHEMA_PATH = REPO_ROOT / "docs/schemas/workspace-status.json" + + +def test_workspace_status_command_matches_published_schema(tmp_path: Path) -> None: + home = tmp_path / "home" + base_home = tmp_path / "base" + workspace = tmp_path / "workspace" + manifest_path = tmp_path / "workspace.yaml" + home.mkdir() + base_home.mkdir() + workspace.mkdir() + + for args, manifest_backed, expected_project_count in ( + (["status", "--workspace", str(workspace), "--format", "json"], False, 0), + ( + ["status", "--workspace", str(workspace), "--manifest", str(manifest_path), "--format", "json"], + True, + 1, + ), + ): + if manifest_backed: + manifest_path.write_text( + "schema_version: 1\nworkspace:\n name: status-suite\nrepos:\n - name: demo\n", + encoding="utf-8", + ) + (workspace / "demo").mkdir() + (workspace / "demo" / "base_manifest.yaml").write_text( + "project:\n name: demo\nartifacts: []\n", encoding="utf-8" + ) + + result = run_workspace_command(args, base_home, home) + + assert result.returncode == 0 + assert result.stderr == "" + payload = json.loads(result.stdout) + validate_workspace_payload(payload, SCHEMA_PATH) + assert payload["schema_version"] == 1 + assert payload["project_count"] == expected_project_count + if manifest_backed: + assert payload["workspace_manifest"]["name"] == "status-suite" + assert payload["repository_count"] == 1 + + discovery_workspace = tmp_path / "discovery-workspace" + discovery_workspace.mkdir() + (discovery_workspace / "discovered").mkdir() + (discovery_workspace / "discovered" / "base_manifest.yaml").write_text( + "project:\n name: discovered\nartifacts: []\n", encoding="utf-8" + ) + result = run_workspace_command( + ["status", "--workspace", str(discovery_workspace), "--format", "json"], + base_home, + home, + ) + + assert result.returncode == 0 + assert result.stderr == "" + payload = json.loads(result.stdout) + validate_workspace_payload(payload, SCHEMA_PATH) + assert payload["schema_version"] == 1 + assert payload["project_count"] == 1 + assert payload["projects"][0]["name"] == "discovered" diff --git a/tests/test_contract_hardening.py b/tests/test_contract_hardening.py index 196c0bef..fb7e5cad 100644 --- a/tests/test_contract_hardening.py +++ b/tests/test_contract_hardening.py @@ -230,6 +230,7 @@ def test_contract_registry_maps_initial_review_contracts_to_enforcement() -> Non "Workspace agent-brief JSON schema": "tests/contracts/test_workspace_agent_brief_schema.py", "Workspace test JSON schema": "tests/contracts/test_workspace_test_schema.py", "Workspace update JSON schema": "tests/contracts/test_workspace_update_schema.py", + "Workspace status JSON schema": "tests/contracts/test_workspace_status_schema.py", } for contract, enforcement in expected_entries.items(): assert contract in text @@ -259,6 +260,7 @@ def test_contract_registry_rows_have_complete_enforcement_metadata() -> None: "Workspace agent-brief JSON schema", "Workspace test JSON schema", "Workspace update JSON schema", + "Workspace status JSON schema", "Native Windows support boundary", } for row in rows: @@ -280,6 +282,7 @@ def test_contract_runner_composes_existing_policy_checks() -> None: "cli/python/base_projects/tests/test_workspace_pull.py", "tests/contracts/test_workspace_agent_brief_schema.py", "tests/contracts/test_workspace_update_schema.py", + "tests/contracts/test_workspace_status_schema.py", "cli/python/base_release/tests/test_engine.py", "cli/bash/commands/basectl/tests/inspection-json.bats", 'bats --filter "project installer template"', diff --git a/tests/test_stability_tiers_docs.py b/tests/test_stability_tiers_docs.py index fc405f9d..ed348568 100644 --- a/tests/test_stability_tiers_docs.py +++ b/tests/test_stability_tiers_docs.py @@ -11,6 +11,7 @@ AGENT_BRIEF_SCHEMA = REPO_ROOT / "docs" / "schemas" / "workspace-agent-brief.json" WORKSPACE_UPDATE_SCHEMA = REPO_ROOT / "docs" / "schemas" / "workspace-update.json" WORKSPACE_TEST_SCHEMA = REPO_ROOT / "docs" / "schemas" / "workspace-test.json" +WORKSPACE_STATUS_SCHEMA = REPO_ROOT / "docs" / "schemas" / "workspace-status.json" WORKSPACE_MANIFEST_SCHEMA = REPO_ROOT / "docs" / "schemas" / "workspace-manifest.json" @@ -102,8 +103,21 @@ def test_workspace_test_schema_is_registered_as_stable() -> None: assert "test_workspace_test_schema.py" in contracts +def test_workspace_status_schema_is_registered_as_stable() -> None: + schema = WORKSPACE_STATUS_SCHEMA.read_text(encoding="utf-8") + stability = STABILITY_DOC.read_text(encoding="utf-8") + command_reference = COMMAND_REFERENCE.read_text(encoding="utf-8") + contracts = CONTRACTS_DOC.read_text(encoding="utf-8") + + assert '"$schema": "https://json-schema.org/draft/2020-12/schema"' in schema + assert '"schema_version":' in schema + assert "schemas/workspace-status.json" in stability + assert "schemas/workspace-status.json" in command_reference + assert "test_workspace_status_schema.py" in contracts + + def test_workspace_schema_manifest_definition_is_shared() -> None: - schema_paths = (AGENT_BRIEF_SCHEMA, WORKSPACE_UPDATE_SCHEMA, WORKSPACE_TEST_SCHEMA) + schema_paths = (AGENT_BRIEF_SCHEMA, WORKSPACE_UPDATE_SCHEMA, WORKSPACE_TEST_SCHEMA, WORKSPACE_STATUS_SCHEMA) manifest_schema = json.loads(WORKSPACE_MANIFEST_SCHEMA.read_text(encoding="utf-8")) assert manifest_schema["type"] == "object"