repo-standard/CLAUDE.md
Kjell Tore Guttormsen 9fcceb7522 fix(gate): the reader sets a link's level, and a repo's name is its remote
Two defects that only org-wide measurement exposes. Both were found by running
the gate against seventeen repositories, not by reading the code.

Link level follows the reader. 30 of 43 LINK-INTERNAL-MISSING findings sat in
`shared/`, `docs/plan/` and `.claude/` -- session plans, agent working files,
and path-traversal fixtures whose targets are invalid on purpose. All 30 were
ERRORs. portfolio-optimiser scored 19 ERRORs and 15 of them were noise. A gate
that wrong gets switched off, so root documents stay ERROR and everything below
becomes WARN. Only the level moves; the finding keeps its file and line.

The OK line had to move with it: it asserted "every resolvable relative link
resolves" whenever no ERROR was present, which would have printed it beside a
pile of WARNs saying the opposite.

The repo name comes from `git remote get-url origin`. `catalog/` is the working
directory of `ktg-plugin-marketplace`; the basename left it REPO-UNREGISTERED
with zero checks run -- against the one repo every catalog rule depends on. The
scp form is handled because the forge's clone button hands it out, and a bare
host is not a repo name (that test caught a real bug: `https://host/` parsed as
a repo named after the host).

Re-measured: portfolio-optimiser 19 ERROR -> 4, all four genuine. catalog now
reports as `ktg-plugin-marketplace [catalog]` and runs its full rule set.

Still v0.1.0: nothing is pushed, no tag exists, no consumer has seen it.

77 -> 84 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeNsGsWukggfmLQ926WZPx
2026-08-03 19:53:57 +02:00

90 lines
4.9 KiB
Markdown

# repo-standard
Per-repo gate for the `open/` presentation standard, packaged as a marketplace
plugin.
## Context
Two components, one boundary:
- **Engine (`scripts/repo-standard-check.mjs`)** — pure classifiers with all I/O
resolved into their input, mirroring the marketplace's `check-versions.mjs`.
Findings are `ERROR`/`WARN`/`SKIP`/`OK`; exit 1 on `ERROR`. Pinned by
`scripts/repo-standard-check.test.mjs` (`npm test`).
- **Skill (`skills/repo-standard/`)** — the judgement the script cannot encode.
No checking logic lives here; it calls the engine.
`register/repos.json` is the single taxonomy register (name → class, per-class
requirements, and the known non-repo names). Central by design: per-repo copies
would recreate, in data, exactly the drift this plugin exists to remove.
## Invariants
- **This repo has a PUBLIC remote.** `STATE.md` is LOCAL-ONLY and gitignored
from the first commit. Repos on a private remote track theirs — do not carry
that habit across in either direction.
- **The gate sees ONE repo.** Anything needing a view across the whole org
(topic coverage, competing install forms, catalog-vs-forge divergence) does
not belong here. It is measured where the org is enumerated.
- **It records, it does not fix.** Findings first, remediation afterwards.
Patching while measuring is how the inconsistency it detects was produced.
- **`SKIP` is never a pass.** A check that could not run says so and names why.
- **When a check fires many times in one repo, suspect the CHECK.** The first
link pass produced ~30 findings against `llm-security` and all were noise —
regexes in code spans, `file:` URLs, relative paths resolved against the wrong
directory. Strip code before scanning text; resolve links against the file
they sit in. A gate that is wrong this often gets switched off.
- **Two axes on every finding.** Level (`ERROR`/`WARN`/`SKIP`/`OK`) and bucket
(`broken`/`missing`/`weakening`). They are independent — a `weakening` finding
can be an `ERROR`.
- **Class is structural, traits are judgement.** Class is read off the catalog
and the remotes. A trait (`security`) says what the code does, which no remote
knows, and the operator owns that list.
- **Who the reader is decides what is required.** Contributor-facing documents
are required by no class — solo-maintained, and the published stance says so.
Consumer-facing ones (`SECURITY.md`, `LICENSE`, non-goals, limitations) are
untouched by that. This is not a rule against having the others.
- **No CI badge, because there is no CI.** The forge has zero Actions runners
registered (measured). The substitute is one command from a clean clone, said
plainly. A static badge asserting a run is the anti-pattern this gate flags —
and an early draft of this README carried one.
- **Three outcomes on references.** "No match" and "match on a known non-repo"
must stay distinct findings. Collapsing them hides real loss inside correct
text — the exact defect class this gate exists to catch.
- **One API call, anonymous.** The org listing carries description and topics
already; per-repo fetching trips the rate limiter (HTTP 429). It reads without
a token, so the gate works for any reader — a public plugin whose documented
check only runs for its author is a broken plugin.
- **Codepoints, not bytes, not UTF-16 units.** Use `[...s].length`. An em-dash
exposes only the byte layer; astral characters expose the rest.
- **The reader decides a link's level, not just what is required.** Root
documents are the shop window — a dead link there is an `ERROR`. Below the
root it is a `WARN`: that is where session plans, agent working files and
path-traversal fixtures with deliberately invalid targets live. Measured, 30
of 43 findings were down there and all were `ERROR`s.
- **A repo's name is its remote, not its directory.** `catalog/` holds
`ktg-plugin-marketplace`. The basename left it unregistered with zero checks
run, against the one repo every catalog rule depends on.
- **No hook until the rule is precise.** A blocking gate that fails a correct
repository is the mechanism that gets gates switched off.
## Commands
```bash
npm test # 84 tests
node scripts/repo-standard-check.mjs --dir "$PWD" # gate one repo
node scripts/repo-standard-check.mjs --offline # no network call
node scripts/repo-standard-check.mjs --json # machine output
node scripts/repo-standard-check.mjs --refresh # register vs. forge
```
## Release
Polyrepo rule: a version bump is not finished until the tag `vX.Y.Z` is pushed
**and** the catalog `ref` is bumped to it. Use `release-plugin.mjs`, never a
hand-edited `ref`.
`check-versions.mjs` reads the catalog README's per-plugin label, and a **missing**
entry is a silent `null` rather than an error — `release-plugin.mjs` rewrites an
existing heading but cannot create one. A new plugin's catalog README entry has
to be added by hand once.