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: 1 addition & 1 deletion GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ Contributors commit to this repo with signed commits. The SSH-signing setup live

Every repo's GitHub repository details (the About panel) follow a fixed convention so the fleet stays consistent and self-describing.

- **Description** matches the README's **tagline**, its first non-empty line after the `#` H1 heading, as plain text, stripping Markdown links (`[text](url)` and `[text][ref]` become `text`) since a description is not rendered. It is that one line and not the paragraph it opens: a README may carry further paragraphs below the tagline, and no mirror reads them. The README is the source of truth: set the description from it (`gh api -X PATCH repos/<owner>/<repo> -f description=...`), never the reverse. When the current description is *more specific* than the README (a chip revision or variant the README omits), surface the drift to the maintainer rather than silently discarding the detail, and the fix is to sharpen the README so the description follows it. Keep the line at most **100 characters**, Docker Hub's short-description cap and the tightest surface it feeds. For a repo that publishes a Docker image, the **Docker Hub short description** mirrors the same tagline, so one canonical sentence carries to the README, the About panel, and Docker Hub alike. Docker Hub receives it from the About panel, which the docker-readme task reads at publish time, so an About panel left diverged from the README is carried onward rather than corrected there.
- **Description** is one canonical sentence that carries to the README, the About panel, and (for a Docker repo) the Docker Hub short description alike, at most **100 characters**, Docker Hub's short-description cap and the tightest surface it feeds. Once a repo declares `registry/repos.json`'s optional `description` field, that field is the source, itself link-free plain text on one line for the same reason: `repo-config/configure.sh apply` writes it to the About panel directly, and the README's **tagline** (its first non-empty line after the `#` H1 heading) follows it rather than the other way around. A repo that has not adopted the field yet keeps the pre-existing convention, where the README tagline is the source of truth and the About panel is set from it by hand (`gh api -X PATCH repos/<owner>/<repo> -f description=...`). `spec/audit.py`'s `description_findings()` reports drift either way, falling back to the tagline when no field is declared. It is that one line and not the paragraph it opens: a README may carry further paragraphs below the tagline, and no mirror reads them. When the current description is *more specific* than the declared source (a chip revision or variant it omits), surface the drift to the maintainer rather than silently discarding the detail, and the fix is to sharpen the declared source so the other mirrors follow it. Docker Hub receives it from the About panel, which the docker-readme task reads at publish time, so an About panel left diverged from the canonical value is carried onward rather than corrected there.
- **Topics** are optional, and any that are present match the repo's actual content. Do not invent topics to fill the field.
- **Include in the home page**: Releases on, Deployments off, Packages off. These toggles are UI-only, since the REST and GraphQL APIs neither read nor write them, so they are set by hand and cannot be audited through `gh`.

Expand Down
18 changes: 0 additions & 18 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,6 @@ One pull request pointing a hub `uses:` at a hub-owned action, so that the resol
- **Settled** - The resolvability pass reports what it covered on every run, so the hub's zero is visible rather than silent, which is why this is a separate decision rather than a defect in that pass.
- **Settled** - The fleet's `ptr727` pins are live in the downstream repos that consume the action, and `repo_gate.py --root <repo>` from a hub checkout reads them there, so the pass is not idle fleet-wide.

### The Declared Repository Description

One pull request moving the canonical short description into declared data, so every check and every push reads a field rather than parsing a document, and the About panel gets something that writes it.

**State** `decision`. **Touches** [`registry/repos.json`][repos] and its schema, [`spec/audit.py`][audit], and `repo-config/configure.sh`. **Cost** one hub edit, and repos adopt the field one at a time. The tagline rule this cluster once carried shipped on 2026-08-08.

- **Declare the description in [`registry/repos.json`][repos] instead of deriving it by parsing the README.** Every check and every push then reads a field.
- **Blocked by** - Nothing.
- **Issue** - None filed, and the disposition is recorded on [#509][issue-509].
- **Checked** - `develop` at `3d1a0b1` on 2026-08-06, where neither `registry/repos.json` nor `registry/repos.schema.json` carries a `description` key.
- **Open** - Nothing beyond sequencing, which is that this leads and the README shape follows.
- **Settled** - PhotoCleaner#32 measures the cost of parsing, since a workflow step reading the intro at publish time needs nine guards against headings, block quotes, all four list markers, ordered lists, HTML, tables, code, links and the length cap, and every one of them fails the release rather than the tagline.
- **Settled** - The field makes the README intro a third mirror rather than the source, so the audit compares all three against one declared value and `repo-config/configure.sh` sets the About panel from the same field it already sets every other setting from.
- **Settled** - The 100-character cap stays, since Docker Hub's short description is the tightest surface.
- **Settled** - The field is optional at first so the audit falls back to the README intro while repos adopt it, and it needs a schema entry because `registry/repos.schema.json` sets `additionalProperties: false`.
- **Settled** - The ask on the Docker repos meanwhile is only that the parsing step is not propagated further.

### Reducing the Carried Surface Further

One pull request measuring the remaining carried surface against the carry-versus-reach test and moving whatever qualifies, now that the model is settled rather than open.
Expand Down Expand Up @@ -515,7 +498,6 @@ Regenerate [reports/divergences.md][divergences-report] before using it as the w
[issue-456]: https://github.com/ptr727/ProjectTemplate/issues/456
[issue-483]: https://github.com/ptr727/ProjectTemplate/issues/483
[issue-489]: https://github.com/ptr727/ProjectTemplate/issues/489
[issue-509]: https://github.com/ptr727/ProjectTemplate/issues/509
[issue-521]: https://github.com/ptr727/ProjectTemplate/issues/521
[issue-523]: https://github.com/ptr727/ProjectTemplate/issues/523
[issue-558]: https://github.com/ptr727/ProjectTemplate/issues/558
Expand Down
1 change: 1 addition & 0 deletions host-setup/linux/install-tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,7 @@ load_repo_tools() {
end
' "$declaration") ||
die "Cannot read constrained Linux install metadata from $declaration"
[[ -n $rows ]] || return 0
while IFS=$'\t' read -r name manager package; do
[[ -n $name ]] || die "$declaration carries Linux install metadata without a non-empty tool name"
if [[ $manager != "apt" || ! $package =~ ^[a-z0-9][a-z0-9+.-]*$ ]]; then
Expand Down
1 change: 1 addition & 0 deletions registry/repos.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
{
"name": "ProjectTemplate",
"url": "https://github.com/ptr727/ProjectTemplate",
"description": "Agent enablement for a fleet of repositories: autonomy and repeatable quality inside guardrails.",
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
"status": "cataloged",
"types": ["source-only", "docs"],
"groundTruthBranch": "main",
Expand Down
7 changes: 7 additions & 0 deletions registry/repos.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@
"properties": {
"name": { "type": "string", "minLength": 1 },
"url": { "type": "string", "format": "uri", "minLength": 1 },
"description": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"pattern": "^\\S(?:[^\\n\\r]*\\S)?$",
"$comment": "Single line, no leading/trailing whitespace - kept in sync with spec/validate.py's description_errors(). That function also rejects Markdown links, which this pattern does not attempt to express."
},
"status": { "enum": ["cataloged", "backlog", "archived", "excluded"] },
"exclusionReason": { "type": "string", "minLength": 1, "pattern": "\\S" },
"types": { "type": "array", "items": { "type": "string" } },
Expand Down
36 changes: 35 additions & 1 deletion repo-config/configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@ esac
main_ruleset="$script_dir/main.json"
settings_file="$script_dir/settings.json"

# ----- Resolve the declared description (optional, shared by apply and check) -----
# Absence keeps the About panel following the README.
description=""
if [ -f "$registry" ]; then
# Resolved here, not near the top, so a run with no registry (an explicit model, no hub checkout) never needs Python at all.
# The name python3 is not universal: native Windows can register a Microsoft Store stub under that name that resolves on PATH but fails when actually run, so this runs it rather than just checking PATH (docs/host-setup.md).
# The probe itself is spec/resolve_description.py's actual floor (PEP 563, Python 3.7+) rather than an arbitrary version number, so a too-old interpreter fails here with a clear message instead of a bare SyntaxError from the script.
if python3 -c "from __future__ import annotations" >/dev/null 2>&1; then
py_cmd=(python3)
elif py -3 -c "from __future__ import annotations" >/dev/null 2>&1; then
py_cmd=(py -3)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
else
echo "No Python 3.7+ interpreter found (python3 or py -3). See docs/host-setup.md." >&2
exit 1
fi
# Delegates to spec/resolve_description.py rather than a third hand-rolled copy of description_errors().
# A description that passes that check can never contain a newline, so command substitution has nothing to strip.
if ! description="$("${py_cmd[@]}" "$script_dir/../spec/resolve_description.py" "$registry" "$name")"; then
exit 1
fi
fi

# ----- Ruleset id lookup (shared by apply and check) -----
# Map a ruleset name to the id of the first match, leaving it empty when nothing matches.
# It warns on duplicates, and aborts on an API error or at the per_page cap, where a single-fetch lookup is unreliable.
Expand Down Expand Up @@ -158,7 +180,11 @@ cmd_apply() {
payload="$(jq --argjson d "$disc" '. + {has_discussions: $d}' "$settings_file")"
echo "Warning: $repo has no 'main' branch. Leaving default_branch unchanged." >&2
fi
echo "Applying general settings (has_discussions=$disc)"
# Applies only once a repo declares the field (see the resolution above).
if [ -n "$description" ]; then
payload="$(jq --arg desc "$description" '. + {description: $desc}' <<<"$payload")"
fi
echo "Applying general settings (has_discussions=$disc$([ -n "$description" ] && echo ", description from registry/repos.json"))"
printf '%s' "$payload" | gh api --method PATCH "repos/$repo" --input - >/dev/null
# ----- Dependabot alerts + automated security updates -----
gh api --method PUT "repos/$repo/vulnerability-alerts" >/dev/null
Expand Down Expand Up @@ -275,6 +301,14 @@ check_settings() {
if gh api "repos/$repo/branches/main" --jq '.name' >/dev/null 2>&1; then
assert "default_branch = main" test "$(jq -r '.default_branch' <<<"$live")" = main
fi
# $description is empty for two different reasons, told apart below since each needs different follow-up.
if [ -n "$description" ]; then
assert "description = '$description'" test "$(jq -r '.description' <<<"$live")" = "$description"
elif [ ! -f "$registry" ]; then
note "description: no $registry to read (it resolves relative to this script, not from the repo argument). Run from a hub checkout for it to exist, and verify manually."
else
note "description: no matching registry entry or no declared description key for $name (falls back to the README tagline, see GOVERNANCE.md 'Repository Details'). Verify manually."
fi
}

check_security() {
Expand Down
68 changes: 68 additions & 0 deletions scripts/tests/test_resolve_description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Exercise resolve_description()'s registry-shape and fail-loud guards directly."""

from __future__ import annotations

import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "spec"))
import resolve_description


class ResolveDescriptionCase(unittest.TestCase):
"""repo-config/configure.sh's declared-description resolution (spec/resolve_description.py)."""

def test_a_repo_with_no_declared_description_resolves_to_none(self) -> None:
registry = {"repos": [{"name": "Fixture"}]}
self.assertIsNone(resolve_description.resolve_description(registry, "Fixture"))

def test_a_repo_absent_from_the_registry_resolves_to_none(self) -> None:
registry = {"repos": [{"name": "Other"}]}
self.assertIsNone(resolve_description.resolve_description(registry, "Fixture"))

def test_a_valid_declared_description_is_returned(self) -> None:
registry = {"repos": [{"name": "Fixture", "description": "A short tagline."}]}
self.assertEqual(
resolve_description.resolve_description(registry, "Fixture"), "A short tagline."
)

def test_a_duplicate_name_raises_rather_than_picking_one(self) -> None:
registry = {
"repos": [
{"name": "Fixture", "description": "First."},
{"name": "Fixture", "description": "Second."},
]
}
with self.assertRaises(resolve_description.ResolveError):
resolve_description.resolve_description(registry, "Fixture")

def test_an_invalid_declared_description_raises(self) -> None:
registry = {"repos": [{"name": "Fixture", "description": None}]}
with self.assertRaises(resolve_description.ResolveError):
resolve_description.resolve_description(registry, "Fixture")

def test_a_registry_with_no_repos_array_raises_rather_than_reading_as_no_match(self) -> None:
with self.assertRaises(resolve_description.ResolveError):
resolve_description.resolve_description({}, "Fixture")

def test_a_repos_value_that_is_not_a_list_raises(self) -> None:
with self.assertRaises(resolve_description.ResolveError):
resolve_description.resolve_description({"repos": "not-a-list"}, "Fixture")

def test_a_padded_name_that_would_otherwise_match_raises_rather_than_reading_as_absent(
self,
) -> None:
registry = {"repos": [{"name": " Fixture ", "description": "A short tagline."}]}
with self.assertRaises(resolve_description.ResolveError):
resolve_description.resolve_description(registry, "Fixture")

def test_a_case_only_name_mismatch_raises_rather_than_reading_as_absent(self) -> None:
registry = {"repos": [{"name": "fixture", "description": "A short tagline."}]}
with self.assertRaises(resolve_description.ResolveError):
resolve_description.resolve_description(registry, "Fixture")


if __name__ == "__main__":
unittest.main()
74 changes: 74 additions & 0 deletions scripts/tests/test_spec_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,79 @@ def test_checks_an_explicit_whole_intent_file_that_also_lists_sections(self) ->
self.assertEqual(1, len(validate.carried_link_errors(self.root, baseline)))


class DescriptionErrorsCase(unittest.TestCase):
"""registry/repos.json's optional `description` (GOVERNANCE.md "Repository Details")."""

def test_a_plain_short_sentence_is_clean(self) -> None:
self.assertEqual(validate.description_errors("Fixture", "A short tagline."), [])

def test_whitespace_only_is_rejected(self) -> None:
self.assertEqual(
validate.description_errors("Fixture", " "),
["Fixture: description must be a non-empty string"],
)

def test_an_explicit_null_is_rejected_rather_than_read_as_absent(self) -> None:
self.assertEqual(
validate.description_errors("Fixture", None),
["Fixture: description must be a non-empty string"],
)

def test_for_repo_an_absent_key_produces_no_errors(self) -> None:
self.assertEqual(validate.description_errors_for_repo({}, "Fixture"), [])

def test_for_repo_an_explicit_null_is_rejected_rather_than_read_as_absent(self) -> None:
# Locks in the presence-vs-None guard: this regresses to `[]` if it is ever weakened back to `is not None`.
self.assertEqual(
validate.description_errors_for_repo({"description": None}, "Fixture"),
["Fixture: description must be a non-empty string"],
)

def test_a_non_string_is_rejected(self) -> None:
self.assertEqual(
validate.description_errors("Fixture", 42),
["Fixture: description must be a non-empty string"],
)

def test_an_inline_markdown_link_is_rejected(self) -> None:
self.assertEqual(
validate.description_errors("Fixture", "See [docs](https://example.test) for more."),
["Fixture: description carries Markdown links - keep it link-free plain text"],
)

def test_a_reference_style_markdown_link_is_rejected(self) -> None:
self.assertEqual(
validate.description_errors("Fixture", "See [docs][ref] for more."),
["Fixture: description carries Markdown links - keep it link-free plain text"],
)

def test_leading_or_trailing_whitespace_is_rejected(self) -> None:
# Not silently trimmed here, even though spec/audit.py and configure.sh both strip it defensively.
# Rejecting it at the source keeps the registry's own text the exact canonical form every mirror carries.
self.assertEqual(
validate.description_errors("Fixture", " A short tagline. "),
[
"Fixture: description must be plain single-line text with no leading or trailing whitespace"
],
)

def test_an_embedded_newline_is_rejected(self) -> None:
self.assertEqual(
validate.description_errors("Fixture", "A tagline.\nA second line."),
[
"Fixture: description must be plain single-line text with no leading or trailing whitespace"
],
)

def test_exactly_the_cap_is_clean(self) -> None:
self.assertEqual(validate.description_errors("Fixture", "a" * 100), [])

def test_over_the_cap_is_rejected(self) -> None:
self.assertEqual(
validate.description_errors("Fixture", "a" * 101),
["Fixture: description is 101 characters, over the 100-char limit"],
)


if __name__ == "__main__":
unittest.main()
Loading