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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 registry/repos.json
Original file line numberDiff line numberDiff 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.",
"status": "cataloged",
"types": ["source-only", "docs"],
"groundTruthBranch": "main",
Expand Down
7 changes: 7 additions & 0 deletions registry/repos.schema.json
Original file line numberDiff line numberDiff 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
40 changes: 39 additions & 1 deletion repo-config/configure.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,29 @@ esac
main_ruleset="$script_dir/main.json"
settings_file="$script_dir/settings.json"

# ----- Resolve the declared description (optional, shared by apply and check) -----
# Per GOVERNANCE.md "Repository Details", once a repo declares registry/repos.json's `description` field, that field becomes the About panel's source rather than the README.
# The audit's description_findings() (spec/audit.py) measures the README, About, and Docker Hub mirror set against that same field.
# A repo with no declared field is left untouched here, so the README stays its source of truth.
description=""
if [ -f "$registry" ]; then
# Trimmed defensively even though spec/validate.py already rejects an untrimmed value.
# A registry edited ahead of its next validate.py run still resolves to the same canonical value spec/audit.py compares against.
if ! description="$(jq -r --arg n "$name" \
'(.repos[] | select(.name==$n) | .description) // "" | gsub("^\\s+|\\s+$"; "")' "$registry")"; then
Comment thread
coderabbitai[bot] marked this conversation as resolved.
echo "Failed to read description from $registry (invalid JSON?)." >&2
exit 1
fi
# The trim above only strips leading/trailing whitespace, so an embedded newline or carriage return survives it.
# Caught here rather than left to reach `gh api` as a multi-line value.
case "$description" in
*$'\n'* | *$'\r'*)
echo "The declared description for $name in $registry carries an embedded newline. Fix it there (spec/validate.py rejects this once run)." >&2
exit 1
;;
esac
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ----- 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 DownExpand Up@@ -158,7 +181,12 @@ 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)"
# The About description, only once a repo declares registry/repos.json's `description` (see the resolution above).
# Left untouched otherwise, so a repo that has not adopted the field yet keeps its hand-set (or README-derived) description.
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 DownExpand Up@@ -275,6 +303,16 @@ 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
# The About description, only where the registry declares one (see the resolution above).
# A repo that has not adopted the field is a manual-verify note, exactly as secrets are: nothing declared here to check against.
# The two reasons `$description` can be empty are told apart, since "no registry" and "no field for this repo" call for 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 (pass a plain repo argument or run from a hub checkout) - verify manually"
else
note "description: no registry/repos.json description declared for $name - verify manually (falls back to the README tagline, see GOVERNANCE.md 'Repository Details')"
fi
}

check_security() {
Expand Down
52 changes: 52 additions & 0 deletions scripts/tests/test_spec_validate.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -142,5 +142,57 @@ 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_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