Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
58 changes: 57 additions & 1 deletion repo-config/configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,50 @@ 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
# Fails loud on a duplicate name (already a validate.py DEFECT) rather than picking one entry over the other.
if ! match_count="$(jq -r --arg n "$name" '[.repos[] | select(.name==$n)] | length' "$registry")"; then
echo "Failed to read $registry (invalid JSON?)." >&2
exit 1
fi
if [ "$match_count" -gt 1 ]; then
echo "$match_count registry entries named $name in $registry. Resolve the duplicate before its description can be read (spec/validate.py rejects this once run)." >&2
exit 1
fi
if ! declared="$(jq -r --arg n "$name" '.repos[] | select(.name==$n) | has("description")' "$registry")"; then
echo "Failed to read $registry (invalid JSON?)." >&2
exit 1
fi
if [ "$declared" = "true" ]; then
# Exactly one match is already established above, so select() itself yields exactly one value here.
# No trim: this only ever validates the value against spec/validate.py's contract, never normalizes it.
# A non-string value (including an explicit null) resolves to empty here, caught by the same guard.
# -j plus the trailing sentinel keeps command substitution from stripping a genuine trailing newline.
if ! description="$(jq -j --arg n "$name" \
'(.repos[] | select(.name==$n) | .description) | if type == "string" then . else empty end' \
"$registry" && printf x)"; then
echo "Failed to read description from $registry (invalid JSON?)." >&2
exit 1
fi
description="${description%x}"
case "$description" in
"" | [[:space:]]* | *[[:space:]])
echo "The declared description for $name in $registry is not a non-empty string with no leading or trailing whitespace. Fix it there (spec/validate.py rejects this once run)." >&2
exit 1
;;
esac
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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
fi
Comment thread
ptr727 marked this conversation as resolved.
Outdated
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 +202,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 +323,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
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