`VERSION-UNAVAILABLE` (SKIP/notRun) becomes `VERSION-NONE` (OK). A repo that claims no version in any of the four places one can be written down has not left a check un-run — the check ran, saw all four, and found no subject. That is the shape `checkReadmeLanguage` has always answered with OK. 0.8.0 deferred this and recorded the reason in the engine: re-levelling "moves a repo's status". Measured false across 19 clones — an added OK cannot worsen the worst *judged* finding, and all three repos emitting the code already read OK. Status moves only for a repo whose entire finding set was skips. OK cannot bless a real gap here: no class requires a version file, and a `plugin` missing its manifest is an independent FILE-MISSING ERROR. Renamed, not just re-levelled: `-UNAVAILABLE` is this engine's naming for a notRun skip, and one OK-level `-UNAVAILABLE` would mislead exactly the reader this change serves. A source scan keeps the old name out. A/B sweep, both engines offline, 19 clones: 223 judged findings, every status and buckets identical; 3 conversions (notChecked 14→13, 3→2, 14→13), byDesign untouched. 170 tests. `--refresh`: register 21 = forge 21. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CTNGvxzCLZp3UXfibYG4kb
279 lines
14 KiB
Markdown
279 lines
14 KiB
Markdown
# repo-standard
|
|
Per-repo gate for the open/ presentation standard: README first screen, install block, files required by the repo's class, and dead repo references.
|
|
|
|
A repository can be substantially good and still read as abandoned. The gap is
|
|
almost never the code — it is the first screen, an install path that stops
|
|
halfway, and references to a name that was retired two renames ago. This plugin
|
|
checks that surface in one repository and reports what it finds.
|
|
|
|
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See the [marketplace governance](https://git.fromaitochitta.com/open/ktg-plugin-marketplace/src/branch/main/GOVERNANCE.md) for the full model.
|
|
|
|
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
|
|
|

|
|

|
|

|
|

|
|
|
|
**Status:** maintained, solo. The public API is the gate's finding codes and the
|
|
register schema; both can still change before 1.0. There is no CI — the forge
|
|
has no Actions runner — so the test claim is one you run yourself, in one
|
|
command, from a clean clone: `npm test`. A badge asserting it would be a claim
|
|
dressed as evidence.
|
|
|
|
## Install
|
|
|
|
Use the `https://` form. The forge UI's clone button hands out an `ssh://` URL,
|
|
and `marketplace add` answers it with `Invalid git URL` — a message that never
|
|
mentions the protocol.
|
|
|
|
```bash
|
|
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
|
|
claude plugin install repo-standard@ktg-plugin-marketplace
|
|
```
|
|
|
|
Or enable it directly in `~/.claude/settings.json` — a working second path, not
|
|
a replacement for the two commands above:
|
|
|
|
```json
|
|
{ "enabledPlugins": { "repo-standard@ktg-plugin-marketplace": true } }
|
|
```
|
|
|
|
## Requirements
|
|
|
|
Node 18 or newer. No dependencies. Two network calls: the org listing (for a
|
|
repo's published description) and the catalog manifest (to confirm the install
|
|
command resolves). Both read anonymously, so no token is needed, and `--offline`
|
|
skips both — the checks that depended on them then report `SKIP`, not `OK`.
|
|
Those land as `notRun` and clear on a networked re-run, so an offline pass is
|
|
never a fully judged one.
|
|
|
|
## What it does
|
|
|
|
Run it inside a repository:
|
|
|
|
```bash
|
|
node scripts/repo-standard-check.mjs --dir "$PWD"
|
|
```
|
|
|
|
The repository's **class** decides what each check means:
|
|
|
|
| Check | What fails it |
|
|
| --- | --- |
|
|
| First screen | no H1 on line 1, or the line under it is not the published description. An H1 that merely differs from the repo name is a `WARN` — that is a naming choice, not a defect |
|
|
| Install block | the form for this class is missing, incomplete, shown over `ssh://`, or points at the wrong marketplace |
|
|
| Install truth | the plugin is not pinned in the catalog, so the documented command cannot succeed for anyone |
|
|
| Required headings | `## Install`, `## Non-goals`, `## Changelog` — per class. Present at the wrong depth is its own finding |
|
|
| Required files | a file this class (or trait) needs is absent |
|
|
| Repo references | an `open/<name>` in URL position resolves to nothing |
|
|
| Relative links | a link points at a file that is not tracked |
|
|
| Licence claim | the README cites a licence the repo has no file for |
|
|
| Badges | a static image badge — any host — asserts a test, build or coverage run that nothing verifies |
|
|
| Badge count | more than five badges, the inflection measured by Trockman et al. (ICSE 2018) past which a badge row reads as clutter rather than as evidence. `WARN` only — the source cannot carry a hard limit |
|
|
| README language | the prose is not in the language this repo's readers were declared to speak |
|
|
| Boilerplate | template text nobody filled in |
|
|
| Version consistency | manifest, README badge, newest CHANGELOG entry and the git tag disagree |
|
|
| Description | empty, or past the length bound |
|
|
|
|
Findings carry two independent things: a **level** (`ERROR`, `WARN`, `SKIP`,
|
|
`OK`) and a **bucket**, which is what you actually triage on:
|
|
|
|
- **broken now** — a stranger is blocked or misled
|
|
- **missing** — an expected artefact is absent
|
|
- **weakening** — present and working, but it reads as amateur
|
|
|
|
The process exits 1 on any `ERROR`. A `SKIP` is never a pass and never a
|
|
severity — it is the absence of a verdict, printed apart from the judged
|
|
findings.
|
|
|
|
A repository's own `status` is the worst of the findings that were *judged*, and
|
|
`SKIP` only when nothing was. A skip cannot outrank twelve checks that did run.
|
|
|
|
Coverage rides beside it on its own axis, and since 0.8.0 that axis has two
|
|
values, because a skip is one of two unrelated facts:
|
|
|
|
- **not judged, by design** — the check saw the thing and declined. A link that
|
|
leaves the repository; a dead link inside a fixture path. It can never become
|
|
a verdict, and nobody has an action.
|
|
- **could not run** — a re-run or an operator action turns it into a verdict.
|
|
An unreachable catalog, an unregistered repo, a repo with no tags.
|
|
|
|
Merging them made clean repositories read as unread. Measured across 19 clones,
|
|
35 of 39 skips were the first kind, so `portfolio-optimiser — OK · 11 not
|
|
checked` announced eleven unread checks when all eleven were links the gate
|
|
declines to judge on purpose.
|
|
|
|
A third shape is not a skip at all and must not be filed as one: the check ran,
|
|
saw everything, and found **no subject** to judge. A repository that claims no
|
|
version anywhere is `VERSION-NONE` at `OK` since 0.9.0 — the same answer
|
|
`LANGUAGE` gives a README with no running prose. It had been `notRun`, which
|
|
told three correctly-built repositories that something about them was still
|
|
unread. What remains under `notRun` is genuinely actionable: an unreachable
|
|
catalog, an unregistered repo, a repo that has not been tagged yet.
|
|
|
|
The summary line names only what someone has an action on:
|
|
|
|
```
|
|
✓ portfolio-optimiser [standalone] — OK (repo-standard v0.9.0 @<sha>)
|
|
✗ some-plugin [plugin] — ERROR · 2 not run (repo-standard v0.9.0 @<sha>)
|
|
· stranger — SKIP · 1 not run (repo-standard v0.9.0 @<sha>)
|
|
```
|
|
|
|
The deliberate ones are not silenced: they keep their own `NOT JUDGED` heading
|
|
in the detail output, beside `NOT CHECKED`. A decision nobody can see reads
|
|
exactly like a check that silently stopped running.
|
|
|
|
`--json` carries `skips: { byDesign, notRun }` beside `status`. `notChecked` is
|
|
unchanged, and still the numeric total of both — the counts are **findings**,
|
|
not checks, one per un-judged item. Absent `skips` means an engine older than
|
|
0.8.0 and absent `notChecked` one older than 0.7.0; neither absence is zero.
|
|
|
|
### Traits — a second axis
|
|
|
|
Class is structural. A **trait** is about what the code does, which no remote can
|
|
tell you. `security` attaches the obligations a tool takes on by handling
|
|
untrusted input: a `SECURITY.md` with a real disclosure channel, and a
|
|
`## Known limitations` section. Traits live in the register and are the
|
|
maintainer's judgement, not a reading.
|
|
|
|
### Locales — a third axis
|
|
|
|
Class is structural, a trait is what the code *does*, and a locale is who the
|
|
code is *for*. English is the default and is not listed. A repo aimed only at a
|
|
Norwegian readership is declared `nb` in the register, and is then wrong in
|
|
English rather than right — the standard's own principle is that who the reader
|
|
is decides what is required, and language is the first thing that decides.
|
|
|
|
Detection is a stopword-frequency comparison over prose with code stripped, so a
|
|
Norwegian flag name in a shell example cannot decide what the document is. It
|
|
answers which language dominates and nothing else: a README can pass this and
|
|
still be badly written. Where the prose is too evenly bilingual to call, the
|
|
finding is a `SKIP`, and a `notRun` one — the question is live and unanswered,
|
|
and fixing the prose or the register's `locales` entry answers it. Where there is no
|
|
running prose at all, it is an `OK`: nothing claims a language, and a thin README
|
|
is the first-screen check's business, not this one's.
|
|
|
|
### Registered titles — where a decision gets written down
|
|
|
|
A README H1 that is a human title rather than the repo name is a naming choice
|
|
the maintainer owns, and the standard prescribes `# Title` for every class. But
|
|
until a YES could be *recorded*, the gate reported the same six warnings every
|
|
round, and there was no way to tell "this was decided" from "nobody has looked".
|
|
|
|
An optional `titles` entry in the register is that record. Set it, and an H1
|
|
matching it is an `OK`. Leave it out, and the warning stands exactly as before.
|
|
The point is the asymmetry: once the deliberate titles are registered, the repos
|
|
nobody has ruled on are the only ones still warning.
|
|
|
|
An H1 that matches neither the repo name nor a registered title is still a
|
|
warning, and the message names both — that is drift in one of the two, not a
|
|
title.
|
|
|
|
### When equality is the wrong demand
|
|
|
|
`README-DESC` requires the README's opening line to be the forge description
|
|
verbatim, because for an ordinary repo the two describe the same subject and any
|
|
gap between them is drift on a published surface.
|
|
|
|
The `org-profile` class is the one place that premise fails. Its README is the
|
|
organisation's landing page; the forge description describes the repository.
|
|
Both are correct about their own subject, so it is the *equality* that does not
|
|
apply, not either text — and a landing page's opening line carries a link, while
|
|
the description field renders as plain text, so satisfying the old check would
|
|
have meant putting raw markdown on a real surface.
|
|
|
|
A class turns the demand off with `readme_desc_match: false` in the register.
|
|
The exemption is still reported as a finding that names the reason: an exception
|
|
nobody can see is indistinguishable from a check that silently stopped running.
|
|
|
|
### What is deliberately not required
|
|
|
|
`CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` and `MAINTAINERS.md` are required by no
|
|
class. This project is solo-maintained and says so publicly; contributor-facing
|
|
documentation for something that accepts no contributors is theatre, and a code
|
|
of conduct with an unattended placeholder address is worse than none — it is a
|
|
visible unfinished template. Consumer-facing documents are untouched by that.
|
|
Being solo is not a reason to skip a `SECURITY.md`; someone finding a hole still
|
|
needs somewhere to send it.
|
|
|
|
### The class decides what is required
|
|
|
|
The class is read off the catalog and the remotes; it is structural, not a
|
|
judgement. It lives in `register/repos.json`.
|
|
|
|
| Class | Install form | Requires |
|
|
| --- | --- | --- |
|
|
| plugin | `marketplace add` **and** a CLI install command | README, LICENSE, CHANGELOG, plugin manifest |
|
|
| catalog | `marketplace add` only — it *is* the marketplace | + GOVERNANCE, CONVENTIONS |
|
|
| shared-asset | how to vendor it; never a plugin install line | README, LICENSE |
|
|
| standalone | pip/uv | README, LICENSE |
|
|
| org-profile | none | README |
|
|
|
|
A flat standard across every class would demand a roadmap from a five-line
|
|
profile. That is how a gate teaches people to switch it off.
|
|
|
|
### Three outcomes on references, not two
|
|
|
|
"Matches no repository" and "matches something that is deliberately not a
|
|
repository" are different findings. The second class is real and it is common:
|
|
a retired name kept alive on purpose in prose, a reserved namespace occupying a
|
|
repo-shaped path, a published package name that was never a repo. If those share
|
|
an outcome with genuine dead links, the genuine ones hide inside a pile of
|
|
correct text.
|
|
|
|
Only names in **URL position** are treated as references, which excludes prose,
|
|
paths and directory names in one move. The `.git` suffix is normalised first —
|
|
without that, a raw scan turns three dead names into about twenty.
|
|
|
|
### Every run says which engine produced it
|
|
|
|
The header and the `--json` output carry the engine's version *and* the commit
|
|
it ran from:
|
|
|
|
```
|
|
✓ voyage [plugin] — OK (repo-standard v0.7.0 @10ad125)
|
|
```
|
|
|
|
Both, because they answer different questions. A stale plugin cache once served
|
|
an old engine against a current repo and the output looked like a clean pass,
|
|
which is why the version is there. But the version names a *file*: a feature and
|
|
its version bump are two commits, so a worktree holds new behaviour under the
|
|
old number for a window, and a sweep once stamped eighteen files with a version
|
|
that predated the check that produced their findings. Only the commit names the
|
|
code.
|
|
|
|
The commit is read from the engine's own checkout, with no network call. When
|
|
there is none to read — a vendored copy, an unpacked tarball — the JSON field is
|
|
present and `null` rather than missing, so "this engine could not tell you" stays
|
|
distinct from "an older engine that never tried".
|
|
|
|
## Non-goals
|
|
|
|
- **Anything requiring a view across every repository at once.** This gate sees
|
|
one repo. A README can look correct from the inside and only turn out wrong
|
|
when a dozen are compared. Org-wide divergence is measured where the org is
|
|
enumerated, not here.
|
|
- **Fixing while measuring.** It records findings; you fix them afterwards.
|
|
Patching under measurement is how inconsistency accumulates unnoticed.
|
|
- **Deciding whether to rename a repo, or whether a roadmap is real.** Those are
|
|
operator calls. The gate brings evidence to them.
|
|
- **Blocking commits.** There is no hook. A gate that fails a correct repository
|
|
is the mechanism that gets gates switched off, so this one earns that role
|
|
before it takes it.
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
npm test
|
|
```
|
|
|
|
170 tests over the pure classifiers. The reference fixtures are measured false
|
|
positives, each with its expected verdict — the six that produced the
|
|
three-outcome reference rule, plus the noise sources found by running the gate
|
|
against a real repository: regexes inside code spans that are markdown links to
|
|
a naive scanner, `file:` URLs, and relative links resolved against the wrong
|
|
directory.
|
|
|
|
## Changelog
|
|
|
|
See [CHANGELOG.md](CHANGELOG.md).
|