fix(garden): keep indentation without frontmatter, reject malformed manifest entries

Three findings on PR #676, all reproduced first.

normalized_agent_text stripped leading whitespace before deciding whether
frontmatter existed, so two frontmatter-free agents differing only by
indentation compared equal. Blank lines ahead of a frontmatter block are
formatting; ahead of anything else they are body content. Same mistake as the
body.strip() fix in the previous commit, one level up.

A plugins list holding a non-object entry was counted as valid, letting garbage
drive an error-severity stale-count finding. The count is unknown unless every
entry is an object.

check_marketplace_consistency then crashed outright on that input with
AttributeError, and on a non-object root as well. Both now report
MARKETPLACE_SHAPE and carry on. That crash predates this PR.
This commit is contained in:
Seth Hobson
2026-08-22 20:58:23 -04:00
parent 8a02f34312
commit aaf76f6223
2 changed files with 90 additions and 3 deletions
+29 -3
View File
@@ -392,7 +392,26 @@ def check_marketplace_consistency(report: Report) -> None:
# missing LOCAL entries as orphans — externals legitimately don't have a plugins/<name>/.
local_entries: dict[str, dict] = {}
external_names: set[str] = set()
for entry in data.get("plugins", []):
entries = data.get("plugins", []) if isinstance(data, dict) else None
if not isinstance(entries, list):
report.add(
kind="MARKETPLACE_SHAPE",
severity="error",
path=MARKETPLACE_JSON,
message="expected an object with a `plugins` list at the top level",
fix='Restore the manifest shape: {"plugins": [ ... ]}.',
)
return
for position, entry in enumerate(entries):
if not isinstance(entry, dict):
report.add(
kind="MARKETPLACE_SHAPE",
severity="error",
path=MARKETPLACE_JSON,
message=f"plugins[{position}] is {type(entry).__name__}, expected an object",
fix="Remove the entry or give it the usual name/source/description fields.",
)
continue
name = entry.get("name")
if not name:
continue
@@ -437,7 +456,12 @@ def normalized_agent_text(text: str) -> str:
a closing `---` at end of file, and a trailing space after a delimiter from
reading as drift. A `name:` line in the body is body content and still counts.
"""
text = text.lstrip(BOM).lstrip()
text = text.lstrip(BOM)
trimmed = text.lstrip()
# Blank lines ahead of a frontmatter block are formatting. Ahead of anything else
# they are body content, and stripping them would hide an indentation difference.
if trimmed.startswith("---"):
text = trimmed
fields, body = parse_frontmatter(text)
fields.pop("name", None)
rendered = "\n".join(f"{key}: {fields[key]!r}" for key in sorted(fields))
@@ -465,7 +489,9 @@ def actual_counts(report: Report) -> dict[str, int | None]:
# Valid JSON of the wrong shape is still an unknown count, not a crash.
if isinstance(manifest, dict):
entries = manifest.get("plugins")
if isinstance(entries, list):
# A list holding a non-object entry is malformed. Counting it would let
# garbage drive an error-severity finding.
if isinstance(entries, list) and all(isinstance(e, dict) for e in entries):
plugins = len(entries)
return {
"plugins": plugins,
+61
View File
@@ -9,6 +9,7 @@ import pytest
from tools.doc_gardener import (
CHECKS,
Report,
actual_counts,
check_agent_divergence,
check_codex_skill_caps,
check_dead_links,
@@ -491,6 +492,20 @@ class TestDocCounts:
assert len(stale) == 1
assert "30 agents" in stale[0].message
def test_malformed_plugin_entry_makes_the_count_unknown(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
"""A plugins list holding a non-object must not drive an error-severity count."""
_patch_paths(monkeypatch, tmp_path)
_write_counts_fixture(tmp_path, plugins=12, agents=34)
(tmp_path / ".claude-plugin" / "marketplace.json").write_text('{"plugins": [null, null]}')
(tmp_path / "README.md").write_text("We ship 99 plugins today.\n")
report = Report()
check_doc_counts(report)
assert [f for f in report.findings if f.kind == "STALE_COUNT"] == []
assert actual_counts(Report())["plugins"] is None
def test_docs_subtotals_are_not_scanned(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""Per-category subtotals under docs/ legitimately differ from the totals."""
_patch_paths(monkeypatch, tmp_path)
@@ -679,6 +694,20 @@ class TestAgentDivergence:
check_agent_divergence(report)
assert report.findings == []
def test_indentation_without_frontmatter_is_content(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
"""Leading blanks are only formatting when they sit ahead of frontmatter."""
_patch_paths(monkeypatch, tmp_path)
for plugin, body in (("alpha", " prose here\n"), ("beta", "prose here\n")):
agents_dir = tmp_path / "plugins" / plugin / "agents"
agents_dir.mkdir(parents=True, exist_ok=True)
(agents_dir / "reviewer.md").write_text(body)
report = Report()
check_agent_divergence(report)
assert [f.kind for f in report.findings] == ["AGENT_BODY_DIVERGENT"]
def test_groups_variants_in_message(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""Three copies sharing two bodies report as 3 copies / 2 versions."""
_patch_paths(monkeypatch, tmp_path)
@@ -732,3 +761,35 @@ class TestUnreadableFiles:
unreadable = [f for f in report.findings if f.kind == "UNREADABLE_FILE"]
assert unreadable
assert unreadable[0].severity == "error"
# ── Marketplace shape ────────────────────────────────────────────────────────
class TestMarketplaceShape:
def test_non_object_entry_is_reported_not_raised(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_paths(monkeypatch, tmp_path)
mp = tmp_path / ".claude-plugin"
mp.mkdir(parents=True, exist_ok=True)
(mp / "marketplace.json").write_text('{"plugins": [null]}')
(tmp_path / "plugins").mkdir(exist_ok=True)
report = Report()
check_marketplace_consistency(report) # must not raise
shape = [f for f in report.findings if f.kind == "MARKETPLACE_SHAPE"]
assert shape and "plugins[0] is NoneType" in shape[0].message
def test_non_object_root_is_reported_not_raised(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_patch_paths(monkeypatch, tmp_path)
mp = tmp_path / ".claude-plugin"
mp.mkdir(parents=True, exist_ok=True)
(mp / "marketplace.json").write_text("[]")
(tmp_path / "plugins").mkdir(exist_ok=True)
report = Report()
check_marketplace_consistency(report) # must not raise
assert [f for f in report.findings if f.kind == "MARKETPLACE_SHAPE"]