repo-standard/README.md
Kjell Tore Guttormsen 1dfd408ffc feat(engine)!: "no version to compare against" is a verdict, not a skip
`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
2026-08-09 22:05:21 +02:00

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.*
![Version](https://img.shields.io/badge/version-0.9.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Skills](https://img.shields.io/badge/skills-1-orange)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
**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).