From 4095ad6151d7134b97d814651b5c8bad818f3e79 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 22 Aug 2026 10:18:02 -0700 Subject: [PATCH 1/7] Declare the Repository Description in registry/repos.json (#639) Implements TODO.md's 'The Declared Repository Description' cluster, the prerequisite #639 itself named: registry/repos.json gains an optional per-repo `description` field (schema + spec/validate.py shape check), repo-config/configure.sh writes it to the GitHub About panel from `apply` and asserts it from `check`, and spec/audit.py's newly extracted description_findings() treats the declared field as canonical once present, measuring the README tagline, the About panel, and the Docker Hub short description against it. A repo with no declared field keeps the prior README-is-the-source-of-truth behavior unchanged. This closes the README-to-About hop #639 reported as the one mirror nothing writes, for any repo that adopts the field. ProjectTemplate adopts it here as the pilot. GOVERNANCE.md 'Repository Details' and spec/readme-structure.md are updated to describe the new precedence, and the completed TODO.md cluster is removed per its own convention. Fixes #639 --- GOVERNANCE.md | 2 +- TODO.md | 18 --- registry/repos.json | 1 + registry/repos.schema.json | 1 + repo-config/configure.sh | 26 +++- spec/audit.py | 269 ++++++++++++++++++++++++++----------- spec/readme-structure.md | 4 +- spec/validate.py | 10 ++ 8 files changed, 230 insertions(+), 101 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 75edfc1e..ca0a4128 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -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// -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: `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, plain text with Markdown links stripped since a description is not rendered) 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// -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`. diff --git a/TODO.md b/TODO.md index 0ea1cb91..4e4d3159 100644 --- a/TODO.md +++ b/TODO.md @@ -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 ` 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. @@ -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 diff --git a/registry/repos.json b/registry/repos.json index 9347dfbb..d3ea07aa 100644 --- a/registry/repos.json +++ b/registry/repos.json @@ -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", diff --git a/registry/repos.schema.json b/registry/repos.schema.json index be6df3a5..69b57067 100644 --- a/registry/repos.schema.json +++ b/registry/repos.schema.json @@ -47,6 +47,7 @@ "properties": { "name": { "type": "string", "minLength": 1 }, "url": { "type": "string", "format": "uri", "minLength": 1 }, + "description": { "type": "string", "minLength": 1, "maxLength": 100 }, "status": { "enum": ["cataloged", "backlog", "archived", "excluded"] }, "exclusionReason": { "type": "string", "minLength": 1, "pattern": "\\S" }, "types": { "type": "array", "items": { "type": "string" } }, diff --git a/repo-config/configure.sh b/repo-config/configure.sh index 2972e361..c9cd6b05 100755 --- a/repo-config/configure.sh +++ b/repo-config/configure.sh @@ -64,6 +64,18 @@ esac main_ruleset="$script_dir/main.json" settings_file="$script_dir/settings.json" +# ----- Resolve the declared description (optional, shared by apply and check) ----- +# Per TODO.md "The Declared Repository Description", 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 it keeps the README-is-the-source-of-truth behavior it had before. +description="" +if [ -f "$registry" ]; then + if ! description="$(jq -r --arg n "$name" '(.repos[] | select(.name==$n) | .description) // ""' "$registry")"; then + echo "Failed to read description from $registry (invalid JSON?)." >&2 + 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. @@ -158,7 +170,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 @@ -275,6 +292,13 @@ 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. + if [ -n "$description" ]; then + assert "description = '$description'" test "$(jq -r '.description' <<<"$live")" = "$description" + 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() { diff --git a/spec/audit.py b/spec/audit.py index 7006ba2e..4be247ff 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -1285,6 +1285,111 @@ def readme_shield_findings(text, model, entry): return findings +def description_findings(doc_texts, entry, live, slug): + """The README title/tagline, the GitHub About description, and the Docker Hub short description, checked as one mirror set. + + Per spec/readme-structure.md item 1 and GOVERNANCE.md "Repository Details", the H1 is the repo name. The tagline + after it, the first line of the intro region, is a link-free plain sentence of at most 100 characters that + carries verbatim to the GitHub About description, and on a docker repo to the Docker Hub short description. Any + further paragraph is free prose no mirror reads, which is why only the first line is measured. + + registry/repos.json's optional `description` is the canonical value once a repo declares it (TODO.md "The + Declared Repository Description"); repo-config/configure.sh then writes the About panel from that same field. + A repo that has not adopted it yet still has the README as its source of truth, so the checks below fall back + to the tagline exactly as before. + """ + findings = [] + if "README.md" not in doc_texts: + return findings + title, intro = title_and_intro(doc_texts["README.md"]) + intro_line = tagline(intro) + # The H1 is the repository name, and a hyphenated name may render its hyphens as spaces. + # Use the GitHub API's canonical name, since the registry-URL slug can carry a different case. + repo_name = live.get("name") or slug.split("/")[-1] + declared = (entry.get("description") or "").strip() or None + if not title: + findings.append( + ( + "LETTER", + "readme: no `# ` H1 title - the README opens with `# ` then the tagline (spec/readme-structure.md)", + ) + ) + elif title.replace("-", " ") != repo_name.replace("-", " "): + findings.append( + ( + "LETTER", + f"readme: the H1 title '{title}' is not the repo name '{repo_name}' (a hyphenated name may render its hyphens as spaces) - the H1 is the repository name (spec/readme-structure.md)", + ) + ) + if not intro_line: + findings.append( + ( + "LETTER", + "readme: no tagline after the H1 - the README opens with the title then a one-line description, which doubles as the About description (spec/readme-structure.md)", + ) + ) + return findings + if strip_md_links(intro_line) != intro_line: + findings.append( + ( + "LETTER", + "readme: the tagline carries Markdown links - keep it link-free plain text, it doubles as the repo About description (spec/readme-structure.md)", + ) + ) + readme_want = strip_md_links(intro_line).strip() + if len(readme_want) > 100: + findings.append( + ( + "LETTER", + f"readme: the tagline is {len(readme_want)} characters, over the 100-char limit (Docker Hub's short-description cap, the tightest surface it feeds) - tighten it to one short sentence (spec/readme-structure.md)", + ) + ) + # The declared field wins once a repo has one; every mirror (README, About, Docker Hub) is then measured against it. + # A repo with no declared field keeps the README as the source, exactly as before. + want = declared or readme_want + source = "registry/repos.json" if declared else "the README" + if declared and readme_want != declared: + findings.append( + ( + "LETTER", + f"readme: the tagline ('{readme_want}') does not match the declared description ('{declared}') - registry/repos.json's description is canonical once declared, so the README follows it (GOVERNANCE.md Repository Details)", + ) + ) + desc = (live.get("description") or "").strip() + if desc != want: + findings.append( + ( + "LETTER", + f"description: the About description does not match the {'declared description' if declared else 'README tagline'} (description '{desc}' vs '{want}') - set it from {source}, or sharpen {source} first if the description carries real detail (GOVERNANCE.md Repository Details)", + ) + ) + # Docker Hub short description mirrors the same canonical value, for a repo that publishes a docker image. + # A transient lookup failure surfaces as a DRIFT ("could not verify"), never aborting or silently passing. + # A 404 (image not at the derived name) returns None and is skipped. + if any( + (pt.get("target") if isinstance(pt, dict) else pt) == "docker" + for pt in entry.get("publish", []) + ): + try: + dh = docker_hub_description(slug) + except Exception as e: # noqa: BLE001 + dh = None + findings.append( + ( + "DRIFT", + f"description: could not read the Docker Hub short description to verify it mirrors {source} ({e}) - verify by hand", + ) + ) + if dh is not None and dh.strip() != want: + findings.append( + ( + "LETTER", + f"description: the Docker Hub short description ('{dh.strip()}') does not match '{want}' - set it from {source} (spec/readme-structure.md)", + ) + ) + return findings + + def workspace_cspell_words(text): """True if workspace/settings JSON carries its own cSpell word list - the block cspell.json canonicalizes. @@ -2197,85 +2302,8 @@ def audit_repo(entry, spec, branch=None): ) # --- README title and intro are the one canonical short description --- - # Per spec/readme-structure.md item 1 and GOVERNANCE.md "Repository Details", the H1 is the repo name. - # The tagline after it, the first line of the intro region, is a link-free plain sentence of at most 100 characters that carries verbatim to the GitHub About description, and on a docker repo to the Docker Hub short description. - # Any further paragraph is free prose no mirror reads, which is why only the first line is measured. - # The README is the source of truth. - if "README.md" in doc_texts: - title, intro = title_and_intro(doc_texts["README.md"]) - intro_line = tagline(intro) - # The H1 is the repository name, and a hyphenated name may render its hyphens as spaces. - # Use the GitHub API's canonical name, since the registry-URL slug can carry a different case. - repo_name = live.get("name") or slug.split("/")[-1] - if not title: - findings.append( - ( - "LETTER", - "readme: no `# ` H1 title - the README opens with `# ` then the tagline (spec/readme-structure.md)", - ) - ) - elif title.replace("-", " ") != repo_name.replace("-", " "): - findings.append( - ( - "LETTER", - f"readme: the H1 title '{title}' is not the repo name '{repo_name}' (a hyphenated name may render its hyphens as spaces) - the H1 is the repository name (spec/readme-structure.md)", - ) - ) - if not intro_line: - findings.append( - ( - "LETTER", - "readme: no tagline after the H1 - the README opens with the title then a one-line description, which doubles as the About description (spec/readme-structure.md)", - ) - ) - else: - if strip_md_links(intro_line) != intro_line: - findings.append( - ( - "LETTER", - "readme: the tagline carries Markdown links - keep it link-free plain text, it doubles as the repo About description (spec/readme-structure.md)", - ) - ) - want = strip_md_links(intro_line).strip() - if len(want) > 100: - findings.append( - ( - "LETTER", - f"readme: the tagline is {len(want)} characters, over the 100-char limit (Docker Hub's short-description cap, the tightest surface it feeds) - tighten it to one short sentence (spec/readme-structure.md)", - ) - ) - desc = (live.get("description") or "").strip() - if desc != want: - findings.append( - ( - "LETTER", - f"description: the About description does not match the README tagline (description '{desc}' vs readme '{want}') - set it from the README, or sharpen the README first if the description carries real detail (GOVERNANCE.md Repository Details)", - ) - ) - # Docker Hub short description mirrors the same intro, for a repo that publishes a docker image. - # A transient lookup failure surfaces as a DRIFT ("could not verify"), never aborting or silently passing. - # A 404 (image not at the derived name) returns None and is skipped. - if any( - (pt.get("target") if isinstance(pt, dict) else pt) == "docker" - for pt in entry.get("publish", []) - ): - try: - dh = docker_hub_description(slug) - except Exception as e: # noqa: BLE001 - dh = None - findings.append( - ( - "DRIFT", - f"description: could not read the Docker Hub short description to verify it mirrors the README ({e}) - verify by hand", - ) - ) - if dh is not None and dh.strip() != want: - findings.append( - ( - "LETTER", - f"description: the Docker Hub short description ('{dh.strip()}') does not match the README tagline ('{want}') - set it from the README (spec/readme-structure.md)", - ) - ) + # See description_findings() for the mirror set (README, GitHub About, Docker Hub) and the declared-field precedence. + findings += description_findings(doc_texts, entry, live, slug) # --- README section order, shield classes, and license-shield placement --- # The readme-structure dimension, driven by the declared model in spec/readme-sections.json rather than by prose. @@ -4120,6 +4148,89 @@ def _selftest(): else: print(" ok tagline: the first line of the intro region, further paragraphs excluded") + # description_findings(): the README/About/Docker Hub mirror set, with and without a declared registry field. + desc_readme = {"README.md": "# Fixture\n\nA short tagline.\n"} + desc_cases = [ + ( + "no declared field, About matches the README tagline", + desc_readme, + {}, + {"description": "A short tagline."}, + 0, + ), + ( + "no declared field, About mismatches the README tagline", + desc_readme, + {}, + {"description": "Something else."}, + 1, + ), + ( + "declared field matches both README and About", + desc_readme, + {"description": "A short tagline."}, + {"description": "A short tagline."}, + 0, + ), + ( + "declared field present, README tagline diverges from it", + desc_readme, + {"description": "The declared description."}, + {"description": "The declared description."}, + 1, + ), + ( + "declared field present, About diverges from it", + desc_readme, + {"description": "A short tagline."}, + {"description": "Something else."}, + 1, + ), + ] + for label, doc_texts_fx, entry_fx, live_fx, wantn in desc_cases: + got = description_findings(doc_texts_fx, entry_fx, live_fx, "owner/Fixture") + if len(got) != wantn: + ok = False + print( + f" {'ok ' if len(got) == wantn else 'FAIL'} want={wantn} got={len(got)} description: {label}" + ) + if len(got) != wantn: + for _, t in got: + print(f" {t}") + # The declared field, once present, is what the wording names as the source - not "the README". + declared_mismatch = description_findings( + desc_readme, + {"description": "The declared description."}, + {"description": "Something else."}, + "owner/Fixture", + ) + if not any( + "declared description" in t and "registry/repos.json" in t for _, t in declared_mismatch + ): + ok = False + print(f" FAIL description: declared-field wording -> {declared_mismatch}") + else: + print( + " ok description: a declared field names registry/repos.json as the source, not the README" + ) + # A docker-publishing repo's Docker Hub short description is checked against the same canonical value. + real_dhd = globals()["docker_hub_description"] + globals()["docker_hub_description"] = lambda slug: "A stale Docker Hub blurb." + try: + docker_mismatch = description_findings( + desc_readme, + {"description": "A short tagline.", "publish": [{"target": "docker"}]}, + {"description": "A short tagline."}, + "owner/Fixture", + ) + finally: + globals()["docker_hub_description"] = real_dhd + if not any("Docker Hub short description" in t for _, t in docker_mismatch): + ok = False + print(f" FAIL description: Docker Hub mismatch not reported -> {docker_mismatch}") + else: + print(" ok description: a docker repo's stale Docker Hub short description is reported") + # A ground-truth branch that does not resolve is one error, not a baseline's worth of letters. # Every `?ref=` read would 404 and report each carried file absent, describing the ref, not the repo. # The branch facts are already read at that point, so they are reported rather than dropped. diff --git a/spec/readme-structure.md b/spec/readme-structure.md index babfc7c8..fcaaa32e 100644 --- a/spec/readme-structure.md +++ b/spec/readme-structure.md @@ -6,7 +6,7 @@ The order, the requirement, and the shield sets are declared data in [`readme-se ## Sections and Order -1. **Title (`# `)** - the H1 **is the repository name** (a hyphenated name may render its hyphens as spaces: `Financial-Modeling` -> `Financial Modeling`), then the **tagline** as the first line of the next paragraph. The tagline is a **single sentence, link-free plain text, at most 100 characters**, and it alone is the canonical short description: it carries to the GitHub About description (GOVERNANCE.md "Repository Details"), to the `HISTORY.md` opening, and, for a repo that publishes a Docker image, to the Docker Hub short description. Those surfaces render no Markdown, and Docker Hub caps the short description near 100 characters, the tightest surface, which sets the limit. **Further paragraphs below the tagline are free prose**, carrying whatever a reader needs to understand the project before the fold, under no length or link rule and read by no mirror. The rule is scoped to the one line the mirrors take precisely so that a README is not forbidden from saying anything more about itself above the fold. The audit checks the H1 name, the tagline's length and link-free form, and the mirrors. +1. **Title (`# `)** - the H1 **is the repository name** (a hyphenated name may render its hyphens as spaces: `Financial-Modeling` -> `Financial Modeling`), then the **tagline** as the first line of the next paragraph. The tagline is a **single sentence, link-free plain text, at most 100 characters**, and it is one canonical short description shared with the GitHub About description (GOVERNANCE.md "Repository Details"), the `HISTORY.md` opening, and, for a repo that publishes a Docker image, the Docker Hub short description. Those surfaces render no Markdown, and Docker Hub caps the short description near 100 characters, the tightest surface, which sets the limit. Once a repo declares `registry/repos.json`'s optional `description` field, that field is the canonical value and the tagline follows it. A repo that has not adopted the field keeps the tagline itself as the source. **Further paragraphs below the tagline are free prose**, carrying whatever a reader needs to understand the project before the fold, under no length or link rule and read by no mirror. The rule is scoped to the one line the mirrors take precisely so that a README is not forbidden from saying anything more about itself above the fold. The audit checks the H1 name, the tagline's length and link-free form, and the mirrors. 2. **Build and Distribution (`##`)** - a bullet per distribution channel the project actually ships, each linking where it lives: **Source Code** (the GitHub repo), **Versioned Releases** (GitHub Releases), **Docker Images** (Docker Hub), **NuGet Packages** (NuGet.org), **PyPI Packages** (PyPI.org). List only the channels the project uses, and name a channel for what it ships, so a repo publishing executables writes Binary Releases where one publishing source archives writes Versioned Releases. It carries three sub-sections: - **Build Status (`###`)** - the CI and build status shields. - **Releases (`###`)** - the version shields, one group per channel the project publishes, per "Shields" below. @@ -97,7 +97,7 @@ The rule lives in [`CODESTYLE.md`][codestyle] "Markdown and Spelling", which eve Docker Hub has two text fields: a **short description** (the tagline, capped near 100 characters) that mirrors the README tagline (item 1), and the longer **overview**. A repo that publishes a Docker image keeps a **separate** `Docker/README.md` for the overview: Docker Hub's description has a much smaller size limit than a project README, so it carries a trimmed overview, not the full README. It is published by the docker-readme workflow task, not copied from the root README. -The short description reaches Docker Hub from the **GitHub About panel**, which the docker-readme task reads as `github.event.repository.description`, rather than from the README. That keeps a Markdown parser out of a publish job, and it means the README-to-About hop is the one a human or the audit closes, not CI. Where About has drifted from the README, the publish faithfully carries the drift onward, which is why the audit checks all three surfaces rather than only the two CI touches. +The short description reaches Docker Hub from the **GitHub About panel**, which the docker-readme task reads as `github.event.repository.description`, rather than from the README. That keeps a Markdown parser out of a publish job. `repo-config/configure.sh apply` writes the About panel from `registry/repos.json`'s declared `description` once a repo has one, so that hop is a tool call rather than a hand edit for an adopting repo. A repo that has not declared the field still closes it by hand. Where About has drifted from the canonical value, the publish faithfully carries the drift onward, which is why the audit checks all three surfaces rather than only the two CI touches. diff --git a/spec/validate.py b/spec/validate.py index 446f144e..2af2f578 100755 --- a/spec/validate.py +++ b/spec/validate.py @@ -471,6 +471,16 @@ def check_secret_set(label, entry, need_kind): effective_model = model or default_model or "release" if effective_model == "operational" and eol is None: errors.append(f"{name}: operational repo must declare lineEndings (lf or crlf)") + # Optional per TODO.md "The Declared Repository Description": a repo that has not adopted the field yet is unaffected, since spec/audit.py's description_findings() falls back to the README tagline for it. + # The cap matches Docker Hub's short-description limit, the tightest surface the field feeds. + desc = repo.get("description") + if desc is not None: + if not isinstance(desc, str) or not desc.strip(): + errors.append(f"{name}: description must be a non-empty string") + elif len(desc) > 100: + errors.append( + f"{name}: description is {len(desc)} characters, over the 100-char limit" + ) status = repo.get("status") if status is None: From efd8404d07a5e3853e8c641b0346d5acbddd7b9c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 22 Aug 2026 10:27:46 -0700 Subject: [PATCH 2/7] Address Copilot review: strip-consistent length check, non-whitespace pattern, stale TODO.md refs - spec/validate.py: measure the 100-char cap on the stripped description, matching the value spec/audit.py's description_findings() treats as canonical, so trailing/leading whitespace no longer skews the check. - registry/repos.schema.json: add the \\S pattern to description, the same guard exclusionReason already carries, so a whitespace-only value does not pass minLength alone. - repo-config/configure.sh and spec/audit.py: point the two new cross-references at GOVERNANCE.md "Repository Details" instead of the TODO.md cluster this PR deletes. --- registry/repos.schema.json | 2 +- repo-config/configure.sh | 2 +- spec/audit.py | 8 ++++---- spec/validate.py | 10 ++++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/registry/repos.schema.json b/registry/repos.schema.json index 69b57067..e4c64db8 100644 --- a/registry/repos.schema.json +++ b/registry/repos.schema.json @@ -47,7 +47,7 @@ "properties": { "name": { "type": "string", "minLength": 1 }, "url": { "type": "string", "format": "uri", "minLength": 1 }, - "description": { "type": "string", "minLength": 1, "maxLength": 100 }, + "description": { "type": "string", "minLength": 1, "maxLength": 100, "pattern": "\\S" }, "status": { "enum": ["cataloged", "backlog", "archived", "excluded"] }, "exclusionReason": { "type": "string", "minLength": 1, "pattern": "\\S" }, "types": { "type": "array", "items": { "type": "string" } }, diff --git a/repo-config/configure.sh b/repo-config/configure.sh index c9cd6b05..f207c049 100755 --- a/repo-config/configure.sh +++ b/repo-config/configure.sh @@ -65,7 +65,7 @@ main_ruleset="$script_dir/main.json" settings_file="$script_dir/settings.json" # ----- Resolve the declared description (optional, shared by apply and check) ----- -# Per TODO.md "The Declared Repository Description", once a repo declares registry/repos.json's `description` field, that field becomes the About panel's source rather than the README. +# 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 it keeps the README-is-the-source-of-truth behavior it had before. description="" diff --git a/spec/audit.py b/spec/audit.py index 4be247ff..f333af66 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -1293,10 +1293,10 @@ def description_findings(doc_texts, entry, live, slug): carries verbatim to the GitHub About description, and on a docker repo to the Docker Hub short description. Any further paragraph is free prose no mirror reads, which is why only the first line is measured. - registry/repos.json's optional `description` is the canonical value once a repo declares it (TODO.md "The - Declared Repository Description"); repo-config/configure.sh then writes the About panel from that same field. - A repo that has not adopted it yet still has the README as its source of truth, so the checks below fall back - to the tagline exactly as before. + registry/repos.json's optional `description` is the canonical value once a repo declares it (GOVERNANCE.md + "Repository Details"). repo-config/configure.sh then writes the About panel from that same field. A repo that + has not adopted it yet still has the README as its source of truth, so the checks below fall back to the + tagline exactly as before. """ findings = [] if "README.md" not in doc_texts: diff --git a/spec/validate.py b/spec/validate.py index 2af2f578..d8665711 100755 --- a/spec/validate.py +++ b/spec/validate.py @@ -471,15 +471,17 @@ def check_secret_set(label, entry, need_kind): effective_model = model or default_model or "release" if effective_model == "operational" and eol is None: errors.append(f"{name}: operational repo must declare lineEndings (lf or crlf)") - # Optional per TODO.md "The Declared Repository Description": a repo that has not adopted the field yet is unaffected, since spec/audit.py's description_findings() falls back to the README tagline for it. + # Optional per GOVERNANCE.md "Repository Details": a repo that has not adopted the field yet is unaffected, since spec/audit.py's description_findings() falls back to the README tagline for it. # The cap matches Docker Hub's short-description limit, the tightest surface the field feeds. + # Measured after stripping, the same value spec/audit.py's description_findings() treats as canonical, so a description padded with whitespace is not judged by a length that value never carries. desc = repo.get("description") if desc is not None: - if not isinstance(desc, str) or not desc.strip(): + desc_stripped = desc.strip() if isinstance(desc, str) else "" + if not isinstance(desc, str) or not desc_stripped: errors.append(f"{name}: description must be a non-empty string") - elif len(desc) > 100: + elif len(desc_stripped) > 100: errors.append( - f"{name}: description is {len(desc)} characters, over the 100-char limit" + f"{name}: description is {len(desc_stripped)} characters, over the 100-char limit" ) status = repo.get("status") From e2843bcdc791c3b0a0e296329e59bff872d6212f Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 22 Aug 2026 10:36:37 -0700 Subject: [PATCH 3/7] Address CodeRabbit/qodo review: canonical-by-construction description, no-README coverage - spec/validate.py: require the declared description to already be trimmed, single-line, and link-free (not just checked after stripping), since neither repo-config/configure.sh nor spec/audit.py's description_findings() normalizes it again after the registry. description_errors() is now a standalone, unit-tested function (scripts/tests/test_spec_validate.py). - spec/audit.py: description_findings() no longer skips the About/ Docker Hub check when the README is unreadable or has no tagline - a declared field is canonical on its own and does not need the README to establish it. Two new selftest cases cover a declared field with no README present. - repo-config/configure.sh: check mode's manual-verify note now tells apart "no registry/repos.json to read" from "no description declared for this repo," and the description-resolution comment drops a change-framing phrase. --- repo-config/configure.sh | 5 +- scripts/tests/test_spec_validate.py | 52 +++++++++++++ spec/audit.py | 114 +++++++++++++++++----------- spec/validate.py | 37 ++++++--- 4 files changed, 155 insertions(+), 53 deletions(-) diff --git a/repo-config/configure.sh b/repo-config/configure.sh index f207c049..863b24e0 100755 --- a/repo-config/configure.sh +++ b/repo-config/configure.sh @@ -67,7 +67,7 @@ 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 it keeps the README-is-the-source-of-truth behavior it had before. +# A repo with no declared field is left untouched here, so the README stays its source of truth. description="" if [ -f "$registry" ]; then if ! description="$(jq -r --arg n "$name" '(.repos[] | select(.name==$n) | .description) // ""' "$registry")"; then @@ -294,8 +294,11 @@ check_settings() { 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 diff --git a/scripts/tests/test_spec_validate.py b/scripts/tests/test_spec_validate.py index 1fe70977..0ec1de44 100755 --- a/scripts/tests/test_spec_validate.py +++ b/scripts/tests/test_spec_validate.py @@ -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: configure.sh reads and writes the field raw, with no trimming of its own. + # An untrimmed value would read as a permanent mismatch on every mirror rather than a one-time fix. + 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() diff --git a/spec/audit.py b/spec/audit.py index f333af66..e5f2bc47 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -1297,58 +1297,65 @@ def description_findings(doc_texts, entry, live, slug): "Repository Details"). repo-config/configure.sh then writes the About panel from that same field. A repo that has not adopted it yet still has the README as its source of truth, so the checks below fall back to the tagline exactly as before. + + A declared field's mirrors (About, Docker Hub) are measured against it even where the README itself could + not be read (missing, oversized, or fetched with no inline content) or carries no usable tagline, since the + declared value is canonical on its own and does not need the README to establish it. """ findings = [] - if "README.md" not in doc_texts: - return findings - title, intro = title_and_intro(doc_texts["README.md"]) - intro_line = tagline(intro) - # The H1 is the repository name, and a hyphenated name may render its hyphens as spaces. - # Use the GitHub API's canonical name, since the registry-URL slug can carry a different case. - repo_name = live.get("name") or slug.split("/")[-1] declared = (entry.get("description") or "").strip() or None - if not title: - findings.append( - ( - "LETTER", - "readme: no `# ` H1 title - the README opens with `# ` then the tagline (spec/readme-structure.md)", - ) - ) - elif title.replace("-", " ") != repo_name.replace("-", " "): - findings.append( - ( - "LETTER", - f"readme: the H1 title '{title}' is not the repo name '{repo_name}' (a hyphenated name may render its hyphens as spaces) - the H1 is the repository name (spec/readme-structure.md)", - ) - ) - if not intro_line: - findings.append( - ( - "LETTER", - "readme: no tagline after the H1 - the README opens with the title then a one-line description, which doubles as the About description (spec/readme-structure.md)", + readme_want = None + if "README.md" in doc_texts: + title, intro = title_and_intro(doc_texts["README.md"]) + intro_line = tagline(intro) + # The H1 is the repository name, and a hyphenated name may render its hyphens as spaces. + # Use the GitHub API's canonical name, since the registry-URL slug can carry a different case. + repo_name = live.get("name") or slug.split("/")[-1] + if not title: + findings.append( + ( + "LETTER", + "readme: no `# ` H1 title - the README opens with `# ` then the tagline (spec/readme-structure.md)", + ) ) - ) - return findings - if strip_md_links(intro_line) != intro_line: - findings.append( - ( - "LETTER", - "readme: the tagline carries Markdown links - keep it link-free plain text, it doubles as the repo About description (spec/readme-structure.md)", + elif title.replace("-", " ") != repo_name.replace("-", " "): + findings.append( + ( + "LETTER", + f"readme: the H1 title '{title}' is not the repo name '{repo_name}' (a hyphenated name may render its hyphens as spaces) - the H1 is the repository name (spec/readme-structure.md)", + ) ) - ) - readme_want = strip_md_links(intro_line).strip() - if len(readme_want) > 100: - findings.append( - ( - "LETTER", - f"readme: the tagline is {len(readme_want)} characters, over the 100-char limit (Docker Hub's short-description cap, the tightest surface it feeds) - tighten it to one short sentence (spec/readme-structure.md)", + if not intro_line: + findings.append( + ( + "LETTER", + "readme: no tagline after the H1 - the README opens with the title then a one-line description, which doubles as the About description (spec/readme-structure.md)", + ) ) - ) + else: + if strip_md_links(intro_line) != intro_line: + findings.append( + ( + "LETTER", + "readme: the tagline carries Markdown links - keep it link-free plain text, it doubles as the repo About description (spec/readme-structure.md)", + ) + ) + readme_want = strip_md_links(intro_line).strip() + if len(readme_want) > 100: + findings.append( + ( + "LETTER", + f"readme: the tagline is {len(readme_want)} characters, over the 100-char limit (Docker Hub's short-description cap, the tightest surface it feeds) - tighten it to one short sentence (spec/readme-structure.md)", + ) + ) # The declared field wins once a repo has one; every mirror (README, About, Docker Hub) is then measured against it. - # A repo with no declared field keeps the README as the source, exactly as before. + # A repo with no declared field keeps the README as the source, exactly as before, so with no readable tagline + # there is nothing yet to measure the other mirrors against. want = declared or readme_want + if want is None: + return findings source = "registry/repos.json" if declared else "the README" - if declared and readme_want != declared: + if declared and readme_want is not None and readme_want != declared: findings.append( ( "LETTER", @@ -4186,6 +4193,27 @@ def _selftest(): {"description": "Something else."}, 1, ), + ( + "declared field present, no README at all, About diverges from it", + {}, + {"description": "The declared description."}, + {"description": "Something else."}, + 1, + ), + ( + "declared field present, no README at all, About already matches it", + {}, + {"description": "The declared description."}, + {"description": "The declared description."}, + 0, + ), + ( + "no declared field and no README, nothing to measure against", + {}, + {}, + {"description": "Anything."}, + 0, + ), ] for label, doc_texts_fx, entry_fx, live_fx, wantn in desc_cases: got = description_findings(doc_texts_fx, entry_fx, live_fx, "owner/Fixture") diff --git a/spec/validate.py b/spec/validate.py index d8665711..3094c06a 100755 --- a/spec/validate.py +++ b/spec/validate.py @@ -42,6 +42,10 @@ MARKDOWN_INLINE_LINK = re.compile(r"\]\((?P[^)\s]+)") MARKDOWN_REFERENCE_LINK = re.compile(r"^\[[^]]+\]:\s*(?P\S+)", re.MULTILINE) TEMPLATE_REPOSITORY_URL = "https://github.com/ptr727/ProjectTemplate" +# A description-shaped link use, `[text](url)` or `[text][ref]`, kept in sync with spec/audit.py's strip_md_links(). +# The carried-link regexes above find a definition's target inside a whole document, not a use inside one short string. +DESCRIPTION_LINK_INLINE = re.compile(r"\[([^\]]*)\]\((?:[^()]|\([^()]*\))*\)") +DESCRIPTION_LINK_REF = re.compile(r"\[([^\]]*)\]\[[^\]]*\]") def load(rel): @@ -52,6 +56,29 @@ def is_str_list(v): return isinstance(v, list) and all(isinstance(x, str) for x in v) +def description_errors(name, desc): + """Shape errors for a registry entry's optional `description` (GOVERNANCE.md "Repository Details"). + + Absence is not checked here, since the field is optional. `desc` is only passed in once a repo declares it. + The value must already be in the exact form every mirror carries: a repo carries no way to strip it again. + repo-config/configure.sh reads and writes it raw, with no trimming of its own, and spec/audit.py's + description_findings() strips only the declared field itself, never the README/About/Docker Hub values it + is compared against. A value that were not already trimmed, single-line, and link-free would therefore + read as a permanent mismatch on every mirror rather than as a one-time fix here. + """ + if not isinstance(desc, str) or not desc.strip(): + return [f"{name}: description must be a non-empty string"] + if desc != desc.strip() or "\n" in desc or "\r" in desc: + return [ + f"{name}: description must be plain single-line text with no leading or trailing whitespace" + ] + if DESCRIPTION_LINK_INLINE.search(desc) or DESCRIPTION_LINK_REF.search(desc): + return [f"{name}: description carries Markdown links - keep it link-free plain text"] + if len(desc) > 100: + return [f"{name}: description is {len(desc)} characters, over the 100-char limit"] + return [] + + def markdown_targets(text): """Yield link targets outside fenced blocks.""" visible = [] @@ -472,17 +499,9 @@ def check_secret_set(label, entry, need_kind): if effective_model == "operational" and eol is None: errors.append(f"{name}: operational repo must declare lineEndings (lf or crlf)") # Optional per GOVERNANCE.md "Repository Details": a repo that has not adopted the field yet is unaffected, since spec/audit.py's description_findings() falls back to the README tagline for it. - # The cap matches Docker Hub's short-description limit, the tightest surface the field feeds. - # Measured after stripping, the same value spec/audit.py's description_findings() treats as canonical, so a description padded with whitespace is not judged by a length that value never carries. desc = repo.get("description") if desc is not None: - desc_stripped = desc.strip() if isinstance(desc, str) else "" - if not isinstance(desc, str) or not desc_stripped: - errors.append(f"{name}: description must be a non-empty string") - elif len(desc_stripped) > 100: - errors.append( - f"{name}: description is {len(desc_stripped)} characters, over the 100-char limit" - ) + errors.extend(description_errors(name, desc)) status = repo.get("status") if status is None: From d7d81636e4d88e6efe2aeadee6a391281117bbec Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 22 Aug 2026 10:40:24 -0700 Subject: [PATCH 4/7] Defensively trim the declared description in configure.sh Per Copilot round 2: spec/validate.py and spec/audit.py both treat the stripped value as canonical, so configure.sh now trims it too via a jq gsub, rather than forwarding the raw registry string. Belt-and- suspenders alongside validate.py's now-stricter already-trimmed requirement, for a registry edited ahead of its next validate.py run. --- repo-config/configure.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/repo-config/configure.sh b/repo-config/configure.sh index 863b24e0..bd54fecb 100755 --- a/repo-config/configure.sh +++ b/repo-config/configure.sh @@ -70,7 +70,10 @@ settings_file="$script_dir/settings.json" # A repo with no declared field is left untouched here, so the README stays its source of truth. description="" if [ -f "$registry" ]; then - if ! description="$(jq -r --arg n "$name" '(.repos[] | select(.name==$n) | .description) // ""' "$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 echo "Failed to read description from $registry (invalid JSON?)." >&2 exit 1 fi From 84b4cf74e1272e9d1efdce437ef5929b6db9248d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 22 Aug 2026 10:46:02 -0700 Subject: [PATCH 5/7] Correct docstring/GOVERNANCE claims per Copilot round 3 - spec/validate.py: description_errors()'s docstring wrongly claimed spec/audit.py never normalizes the README/About/Docker Hub side of the comparison - it strips whitespace on every side already, and configure.sh now trims too (d7d8163). Rewritten to state the actual justification: links (audit.py never re-strips the declared value) and already-trimmed (single canonical source text, not reliant on two other layers' defensive stripping to agree). - scripts/tests/test_spec_validate.py: matching test-comment fix. - GOVERNANCE.md: "plain text with Markdown links stripped" reworded to "link-free plain text" - the prior wording could read as permitting links in the tagline as long as they get stripped somewhere, which is backwards. --- GOVERNANCE.md | 2 +- scripts/tests/test_spec_validate.py | 4 ++-- spec/validate.py | 14 +++++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index ca0a4128..7b1d1485 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -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** 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: `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, plain text with Markdown links stripped since a description is not rendered) 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// -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. +- **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: `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, link-free plain text since a description is not rendered) 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// -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`. diff --git a/scripts/tests/test_spec_validate.py b/scripts/tests/test_spec_validate.py index 0ec1de44..c7a29340 100755 --- a/scripts/tests/test_spec_validate.py +++ b/scripts/tests/test_spec_validate.py @@ -167,8 +167,8 @@ def test_a_reference_style_markdown_link_is_rejected(self) -> None: ) def test_leading_or_trailing_whitespace_is_rejected(self) -> None: - # Not silently trimmed: configure.sh reads and writes the field raw, with no trimming of its own. - # An untrimmed value would read as a permanent mismatch on every mirror rather than a one-time fix. + # 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. "), [ diff --git a/spec/validate.py b/spec/validate.py index 3094c06a..70648088 100755 --- a/spec/validate.py +++ b/spec/validate.py @@ -60,11 +60,15 @@ def description_errors(name, desc): """Shape errors for a registry entry's optional `description` (GOVERNANCE.md "Repository Details"). Absence is not checked here, since the field is optional. `desc` is only passed in once a repo declares it. - The value must already be in the exact form every mirror carries: a repo carries no way to strip it again. - repo-config/configure.sh reads and writes it raw, with no trimming of its own, and spec/audit.py's - description_findings() strips only the declared field itself, never the README/About/Docker Hub values it - is compared against. A value that were not already trimmed, single-line, and link-free would therefore - read as a permanent mismatch on every mirror rather than as a one-time fix here. + + Link-free is enforced because spec/audit.py's description_findings() strips Markdown links from the README's + own tagline before comparing, but never re-strips the declared field it compares that tagline against. A + declared value carrying a link would therefore report as a permanent readme mismatch, and repo-config/ + configure.sh would push the literal Markdown source to GitHub's About panel, which does not render it. + + Already trimmed and single-line is enforced so the registry's own text already reads as exactly what every + mirror carries, rather than relying on the whitespace-stripping spec/audit.py and repo-config/configure.sh + each do defensively to keep agreeing with each other. """ if not isinstance(desc, str) or not desc.strip(): return [f"{name}: description must be a non-empty string"] From 9b139b2d46f4d31949b97bb55d5823de80ac5517 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 22 Aug 2026 10:55:13 -0700 Subject: [PATCH 6/7] Guard configure.sh against an embedded newline in the declared description Per a suppressed Copilot finding: the leading/trailing trim added in d7d8163 does not touch an embedded newline or carriage return, which would otherwise reach 'gh api' as a multi-line value if a repo's registry entry is edited ahead of its next spec/validate.py run (the exact scenario that trim is already defensive about). apply/check now fail early with a clear message instead. --- repo-config/configure.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/repo-config/configure.sh b/repo-config/configure.sh index bd54fecb..e5de18eb 100755 --- a/repo-config/configure.sh +++ b/repo-config/configure.sh @@ -77,6 +77,14 @@ if [ -f "$registry" ]; then 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 # ----- Ruleset id lookup (shared by apply and check) ----- From 7dba8f1c75a1a093c80b0a55b098754648db00a7 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 22 Aug 2026 11:02:57 -0700 Subject: [PATCH 7/7] Tighten the schema pattern and clarify GOVERNANCE.md per round-6 review - registry/repos.schema.json: description's pattern now matches spec/validate.py's actual shape check (single line, no leading or trailing whitespace) instead of the looser 'contains a non- whitespace character' check, so an editor validating against the schema doesn't accept a value CI would reject. - GOVERNANCE.md: states that the declared registry field is itself link-free plain text, not only the README tagline it feeds - the prose contract previously covered only the fallback path. Declined a CodeRabbit sentence-length finding on the same bullet: the opt-in sentence-length rule (comment-and-doc-style/SKILL.md) is violated pervasively throughout this file (11+ pre-existing spots, none touched by this PR), so restructuring only this one bullet would be inconsistent with the file's established voice rather than a fix. --- GOVERNANCE.md | 2 +- registry/repos.schema.json | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 7b1d1485..e170a8c4 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -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** 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: `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, link-free plain text since a description is not rendered) 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// -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. +- **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// -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`. diff --git a/registry/repos.schema.json b/registry/repos.schema.json index e4c64db8..4efe29be 100644 --- a/registry/repos.schema.json +++ b/registry/repos.schema.json @@ -47,7 +47,13 @@ "properties": { "name": { "type": "string", "minLength": 1 }, "url": { "type": "string", "format": "uri", "minLength": 1 }, - "description": { "type": "string", "minLength": 1, "maxLength": 100, "pattern": "\\S" }, + "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" } },