Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli/python/base_projects/tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions cli/python/base_projects/workspace_report_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>`, `--manifest <path>`, `--format <text\|csv\|tsv\|yaml\|json>` |
| `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 <path>`, `--manifest <path>`, `--format <text\|csv\|tsv\|yaml\|json>` |
| `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 <path>`, `--manifest <path>`, `--format <text\|csv\|tsv\|yaml\|json>`, `--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 <path>`, `--manifest <path>`, `--format <text\|csv\|tsv\|yaml\|json>`, `--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 <path>`, `--manifest <path>`, `--format <text\|csv\|tsv\|yaml\|json>` |
Expand Down
1 change: 1 addition & 0 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
6 changes: 4 additions & 2 deletions docs/output-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
140 changes: 140 additions & 0 deletions docs/schemas/workspace-status.json
Original file line number Diff line number Diff line change
@@ -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": {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contract clarity (non-blocking). With a manifest, project_count counts only repos whose manifest is valid/invalid. In my probe, 5 records gave project_count: 2 and repository_count: 5. That's existing behavior, but a reader of a schema now marked stable will likely assume project_count == len(projects). A description on project_count and repository_count would capture the rule.

Related: venv, manifest and repo are free-form strings even though the values come from a fixed set (manifest: valid/invalid/missing/unknown; repo: present/missing; ...). Leaving them open is defensible under the additive-change policy, because adding an enum value would otherwise be breaking. Even so, listing the known values in description would help consumers.

"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": {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing doc drift, now part of a published contract (non-blocking, probably a follow-up issue). basectl workspace status never emits python_runtime: engine.py:329 calls workspace_project_statuses(workspace_root, manifest) without probe_venv, and that has been the default since runtime inspection began requiring consent (#2236). A uv project with a .venv reports venv: "present_unverified" and no python_runtime. But docs/python-manifest.md ("Workspace status python_runtime") still says the status JSON includes python_runtime for each ready project.

Keeping it optional in the schema is correct. But a consumer reading the docs alongside this new stable schema will expect a field that never appears. Either fix that doc section (it's the 4th doc surface for this contract), or file an issue to track it.

"$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"
}
}
}
}
}
2 changes: 2 additions & 0 deletions docs/stability-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/contracts/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions tests/contracts/test_workspace_status_schema.py
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage gap (non-blocking). All three scenarios are happy paths with exit 0. The shapes most likely to drift are the non-ok ones, and none is exercised: manifest_path: null for a non-Base repo, a missing required/optional repo (repo: "missing", venv/manifest: "unknown", exit 1), an invalid manifest with issues, and url/default_branch metadata. I checked by hand that all of these validate today, but a future change to them wouldn't be caught. One more manifest case with a missing required repo, a non-Base dir and a URL'd repo (asserting returncode == 1) would cover most of the schema's optional surface.

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"
3 changes: 3 additions & 0 deletions tests/test_contract_hardening.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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"',
Expand Down
16 changes: 15 additions & 1 deletion tests/test_stability_tiers_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down Expand Up @@ -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"
Expand Down
Loading