Compare commits

..

No commits in common. "main" and "v0.2.2" have entirely different histories.

10 changed files with 95 additions and 3173 deletions

View file

@ -1,6 +1,6 @@
{
"name": "repo-standard",
"version": "0.10.1",
"version": "0.2.2",
"description": "Per-repo gate for the open/ presentation standard: README first screen, install block, files required by the repo's class, and dead repo references.",
"author": {
"name": "Kjell Tore Guttormsen"

View file

@ -4,457 +4,6 @@ All notable changes to this project are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
versioning is [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.10.1] — 2026-08-12
### Fixed
- **The skill had drifted behind the engine in the release that banned exactly
that.** 0.10.0 canonized "a new check is not shipped until its row exists" and
shipped with `SKILL.md` still saying "the two network calls" after the third
had been added, and with no mention of `VERIFY-COMMAND` at all. The catalog
pins by tag, so the stale skill was the one an installed reader actually got —
which is why this is a release rather than a note.
It also now names the trap in fixing a `VERIFY-MISSING`: document the command
that already exists, after running it. Writing `npm test` into a README
because the finding mentioned it, with no `scripts.test` behind it, turns a
`WARN` about missing documentation into a false claim on the first screen — a
worse defect than the one being fixed, and one this gate can never catch,
because it runs nothing.
## [0.10.0] — 2026-08-12
### Added
- **`VERIFY-COMMAND` — can a stranger check that this repository works?** The
forge has no Actions runners registered, so this org publishes no CI badge;
the stated substitute is one command a stranger can run from a clean clone. A
repository with something runnable and no such command in its README is a
`WARN` in the `missing` bucket, and the finding names what the repo already
has, so the remedy is one line.
| outcome | level | code |
|---|---|---|
| the README shows a verification command | `OK` | `VERIFY-COMMAND` |
| nothing runnable exists in the repository | `OK` | `VERIFY-NONE` |
| a suite exists and the README shows no command | `WARN` | `VERIFY-MISSING` |
The subject is **measured**, never read off a class: a `scripts.test` entry, or
a tracked file that is unambiguously an executable test — not merely a file
living under `tests/`, which counted golden transcripts and fixtures and would
have made "you have tests a stranger cannot run" false on the loudest findings
it produced. Measured across all 21 registered clones: 10 document a command,
6 do not, and 5 have nothing runnable at all. Those five span three different
classes, so every class-level phrasing of this rule fails a correct repository
somewhere.
The 6-of-21 rate is acceptable for a reason that travels with the rule: the
org publishes exactly one stance on verification, so an undocumented suite is
not a second legitimate convention the way bump-first vs bump-at-release was
for the rejected `VERSION-DRIFT`.
It adds **no API call** — it reads the README and `package.json` — and so has
no `SKIP` at all, unlike every check added since `PIN-DEAD`. It can only ever
fell a MISSING command: nothing here runs anything, so its `OK` says
*documented*, never *passing*.
- **`RELEASE-CURRENT` — does the release page show the version the code is on?**
A stranger who wants a version they can name lands on the forge's release
page, and it is the one surface refs cannot answer: a release is not a ref, so
`git ls-remote` has nothing to report. This is therefore the third API call
per invocation, and the only new one the acquisition model adds.
Both sides are read from the FORGE, never from the clone. Comparing a local
tag against a published release would report `portfolio-optimiser` as having a
stale release when the actual defect is a tag that was never pushed (v1.0.0
local, v0.1.0 published) — a different finding, owned by a different check.
Four outcomes, and three of them are `OK`:
| outcome | level | code |
|---|---|---|
| releases or refs unreadable | `SKIP`/`notRun` | `RELEASE-UNAVAILABLE` |
| no tags on the forge | `OK` | `RELEASE-NONE` |
| tags, no release published | `OK` | `RELEASE-TAGS-ONLY` |
| newest release is the newest tag | `OK` | `RELEASE-CURRENT` |
| newest release lags the newest tag | `WARN`/`weakening` | `RELEASE-STALE` |
The levels come from a measurement across all 22 registered repos
(2026-08-12): 4 have no tags, 2 tag without ever publishing a release, 11 are
current, 5 lag. Those 2 — `ktg-plugin-marketplace` and `llm-security-commons`
— are why zero releases is an `OK` and not a finding. Nothing in a repository
says which of the two legitimate conventions it follows, and a gate that fails
a correct repository is the mechanism that gets gates switched off. That is
the same measurement that rejected `VERSION-DRIFT` one check over.
Lagging is a `WARN` rather than an `ERROR` for two reasons. The remedy is
safe: publishing a release for a tag that already exists moves no published
ref, unlike the remedy `TAG-ANNOTATED` has to withhold from tag history. And
this repository is the worst offender in the org — v0.9.0 tagged, v0.3.0
published — so an `ERROR` would have let the gate settle an operator
question, whether this org publishes releases at all, by exiting 1 on its own
author.
- **`REMOTE-SYNC` — a tag that exists only in the operator's clone.** This is
the blind spot in `VERSION-TAG` rather than a duplicate of it: `VERSION-TAG`
reads LOCAL tags, so a manifest claiming `1.0.0` against an unpushed `v1.0.0`
reads as a clean pass while no stranger can resolve that version.
`portfolio-optimiser` is the measured case, and it read `OK` until this check
existed.
`ERROR`/`broken`, with the remedy named: `git push origin <tag>`. Measured
across all 21 registered clones (2026-08-12): exactly one repo has an unpushed
tag, and none is behind the forge. One subject is what got `BRANCH-STALE`
rejected — the difference is that an unpushed tag is never one of two
legitimate conventions the way tag-only releasing is, the remedy moves no
published ref, and the finding recurs at every release rather than once.
The reverse direction is deliberately not a finding. A clone that has not
fetched lately is behind the forge and nothing about the repository is wrong;
firing there would fail correct repositories on the reader's machine state.
### Changed
- **The API-call count in `CLAUDE.md` is three, updated in the same commit as
the check that changed it.** That line has now gone stale twice; the first
time it stayed stale until a 13-repo shell loop trusted it and tripped the
rate limiter.
- **The "429 at ~40 requests" figure is marked unusable.** The run that produced
it did not record which endpoint class it hit, and org-ops measured ~110
sequential raw reads at 0.4s with zero 429 the same day. Either those are two
different limiters or the older figure is wrong, and the note cannot tell them
apart. One API-axis figure this repo can stand behind was recorded instead: 22
sequential `/api/v1/` calls at 2s pacing, anonymous, zero 429.
- **The README's own surface, which had drifted behind the engine.** Four checks
had shipped with no row in the check table (`PIN-DEAD`, `TAG-ANNOTATED`,
`RELEASE-CURRENT`, `REMOTE-SYNC`), and the Requirements section still said
"two network calls" after the third had been added. A stale count on the
README of the repo whose job is presentation is the defect this gate exists to
find, one file over.
### Not built
- **`RELEASE-ASSETS` — rejected permanently, for having no subject.** No release
in the org carries an asset, but the deciding measurement is the other one: 0
of 21 READMEs mention an asset download at all. Every install path here runs
through `marketplace add`, `plugin install`, `pip`/`uv` or a clone, so no
reader is harmed and no remedy would ever be performed. The fire rate alone
(18 of 18) is a proxy and must not be quoted as the reason.
- **`TAG-SIGNED` — blocked on an operator decision, which is not the same as
rejected.** No repo signs its tags today. If the operator decides this org
signs them, the check acquires its entire subject the same day; a rejection is
permanent, and this is one yes/no away.
## [0.9.0] — 2026-08-09
### Changed
- **`VERSION-UNAVAILABLE` (`SKIP`/`notRun`) is now `VERSION-NONE` at `OK`.** A
repository 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`
("no running prose to judge"), and filing it as a skip told three
correctly-built repositories that something about them was unread.
0.8.0 deferred this deliberately and recorded the reason in the engine:
re-levelling "moves a repo's status". That was measured false. An added `OK`
cannot worsen the worst *judged* finding, so `status` moves only for a repo
whose entire finding set was skips — and all three repos that emit this code
(`ktg-plugin-marketplace`, `playground-design-system`,
`portfolio-optimiser-commons`) already read `OK`. The comment is gone with the
code it justified.
Nor can `OK` here bless a real gap: no class in the register requires a
version file, and a `plugin` missing its manifest is an independent
`FILE-MISSING` `ERROR`.
**The code was renamed, not just re-levelled.** `-UNAVAILABLE` is this
engine's naming for a `notRun` skip (`DESC-UNAVAILABLE`), and one `OK`-level
`-UNAVAILABLE` would mislead exactly the reader this change is for. A source
scan holds the old name out of the engine, so a re-introduction fails the
suite rather than the next census.
Verified before release: 170 tests; 19 clones swept with both engines offline,
223 judged findings, every `status` and `buckets` identical. Exactly three
findings converted — `notChecked` 14→13, 3→2, 14→13, `notRun` 3→2 in each, and
`byDesign` untouched. No repository's `status` moved. Remaining `notRun` across
the org is actionable by construction: `INSTALL-TRUTH` (12), `VERSION-TAG` (1),
plus the two that only appear under `--offline`.
## [0.8.1] — 2026-08-09
### Fixed
- `SKILL.md` glossed `SKIP` as "the check could not run" in its opening list and
then contradicted itself five lines later, where 0.8.0 had added the two
kinds. A session reading only the list would report every `byDesign` skip as
something that failed to run, and send someone to fix fixture links that are
deliberate. Shipping 0.7.0 without its skill update produced the same class of
defect; this is the check that was supposed to stop it happening twice.
- README's `--offline` note said the dependent checks "report `SKIP`, not `OK`"
without saying which kind, now that the summary line only names one of them.
They are `notRun` and clear on a networked re-run — an offline pass is never a
fully judged one, which is the whole reason the qualifier exists.
- README's bilingual-undecidable paragraph likewise names its kind, and the
action that resolves it.
## [0.8.0] — 2026-08-09
### Added
- **Skips split into the two facts they always were.** Every `SKIP` finding now
carries `skip: 'byDesign' | 'notRun'` beside its level, and results carry
`skips: { byDesign, notRun }` beside `status`. `byDesign` is a check that saw
the thing and declined — a link leaving the repository, a dead link in a
fixture path; it can never become a verdict and nobody has an action.
`notRun` is one a re-run or an operator action resolves — an unreachable
catalog, an unregistered repo, a repo with no tags.
0.7.0 took `SKIP` out of the severity lattice, which fixed "clean repos look
skipped". It left both kinds inside one number, which said the same thing
more quietly: measured across 19 clones, 35 of 39 skips were deliberate, so
`portfolio-optimiser — OK · 11 not checked` announced eleven unread checks
when all eleven were fixture and out-of-repo links. org-ops named the split
in its census-05 report without a name for it.
The kind is set where the finding is emitted, never looked up from its code:
`VERSION-TAG` is emitted at `SKIP` with no tags and at `ERROR` with the wrong
one, so a code→kind map would have to re-derive a reason the emission site
already had. An untagged skip counts as `notRun` — the loud side, because a
skip of unknown kind must not inherit "deliberate, nothing to see".
### Changed
- **The summary line names only what someone has an action on.** A repo whose
every skip was deliberate prints an unqualified verdict; `· N not run`
appears only when N > 0. The deliberate ones are not silenced — the detail
output gained a `NOT JUDGED — deliberately outside what this gate decides`
heading beside `NOT CHECKED`. Landing the split in `--json` alone would have
left the human-facing symptom exactly where it was.
- `notChecked` is untouched: still present, still a **number**, still the total
of both kinds. A consumer doing `notChecked > 0` against an object would get
a silent false — the same class of quiet wrong answer this axis exists to
remove. Absent `skips` means an engine older than 0.8.0; absent `notChecked`
one older than 0.7.0. Neither absence is zero.
- `VERSION-UNAVAILABLE` stays `notRun` by decision, not by default. "No package
version to compare against" is arguably a third thing — no *subject* to
judge, the shape `checkReadmeLanguage` answers with `OK` rather than `SKIP`
but re-levelling it moves a repo's `status`, and that is a second behaviour
change. Recorded in the engine so the next reader knows it was decided.
- README example output cites `@<sha>` instead of a commit hash. Three releases
running, those examples went stale against the behaviour they demonstrate; a
placeholder cannot.
Verified before release: 169 tests; 19 clones swept with both engines offline,
220 judged findings and every `status`, `notChecked` and `buckets` identical,
with no finding differing beyond the new tag. The exhaustiveness test scans the
engine source rather than the finding set — a corpus test only sees the sites it
manages to trigger, and the next emission site added would inherit the reader's
default invisibly.
## [0.7.1] — 2026-08-09
### Fixed
- The skill told a reader what a `SKIP` finding means and stopped there, which
was complete before 0.7.0 and is not now: a repo can headline `OK · 3 not
checked`, and a session reporting only the status word would report a green
pass over three unjudged findings. `SKILL.md` now requires the status and the
count to be reported together. Shipping 0.7.0 without this made the plugin
whose purpose is catching stale published documents ship one.
- README's example output carried commits older than the behaviour they
demonstrate — one from 0.6.0 beside a `notChecked` that did not exist in it,
one two releases back. This repo's own invariant is that the version names a
file and only the sha names the code; the examples now cite `10ad125`, the
commit the feature actually shipped in.
### Added
- README states the unit of `notChecked`: it counts un-judged **findings**, not
checks. A repo with many links leaving it scores a high count without a single
check having failed to run, so the bare number reads worse than the repo is.
Named as a known limitation rather than corrected in the string — the fix is
splitting *deliberately not judged* from *could not run*, which is its own
release.
## [0.7.0] — 2026-08-09
### Changed
- **A repo's `status` is the worst *judged* finding, and `SKIP` only when
nothing was judged.** `SKIP` used to rank between `OK` and `WARN`, so one
un-runnable check spoke for every check that ran: a repo with 0 ERROR, 0 WARN
and a dozen `OK` headlined as "skipped". Reported by org-ops against five
repos in census 05 — `ktg-plugin-marketplace`, `okr`,
`playground-design-system`, `portfolio-optimiser`, `commons` — with `okr`
holding the most `OK` in the org and reading as unread. A skip is not a
severity; it is the absence of a verdict, and it cannot be the worst of a set
that contains real ones. "`SKIP` is never a pass" survives intact in the half
of the rule that keeps it: an unregistered repo, or an empty finding set,
still says `SKIP`, because there is nothing else to be worst of.
Measured across all 21 local clones from one saved sweep, so before and after
are computed from the *same* findings rather than from two sweeps of a moving
org: 343 findings before, 343 after, none added or removed. 8 repos moved,
every one `SKIP``OK`. The counts that decide whether a repo needs work did
not move at all — 1 `ERROR` before and after, 3 `WARN` before and after.
Consumers reading `.status` see a changed value domain: a repo that reported
`SKIP` in censuses 0307 may report `OK` from 08 on with an identical finding
set. Raw census files already carry `engineVersion` and `engineCommit`, which
is what distinguishes the two readings.
### Added
- `notChecked` beside `status`, in `--json` and as a `· N not checked` qualifier
on the summary line. Fixing the status alone would have traded "clean repos
look skipped" for "skipped checks look clean", which is the worse direction —
so coverage gets its own axis rather than being folded into the verdict, the
same split the engine already makes between a finding's level and its bucket.
The count is the number of `SKIP` findings, which is what the existing
`NOT CHECKED — these are not passes` section has always listed. An **absent**
`notChecked` means an older engine and prints the pre-0.7.0 line; it is not
zero.
## [0.6.0] — 2026-08-09
### Added
- `titles` in the register: an optional per-repo README title. Set, the H1 is
compared against it and a match is `OK`; unset, the `WARN` stands exactly as
before. A human title was already a valid choice — `docs/rds-v1.md` prescribes
`1. # Title` for every class, not `# repo-name` — but a decided YES had
nowhere to live, so the same 6 `README-H1` WARNs were reported in census 05,
06 and 07 and would have been reported forever. "We decided this is correct"
and "nobody has looked at it" now produce different outcomes. Five titles
registered, each H1 read from the repo rather than copied from the census;
`ai-psychosis` is deliberately left unregistered, so the one repo where a
reader cannot connect title to name stands alone. Measured across all 21
local clones: 6 WARN before, 1 after, no other finding moved.
- `engineCommit` alongside `engineVersion`, in the header line (short) and the
`--json` output (full). The version names a FILE; only the sha names the CODE.
A sweep stamped 18 raw files `0.4.0` while four carried findings from a check
that only exists in 0.5.0 — the feature and the version bump are two commits,
so the worktree held new code under an old number for a window, and the stamp
lied without being broken. Derived from this checkout, no network call. The
field is present-and-`null` when underivable, never absent: an absent key
means an older engine, an explicit `null` means this engine ran and had no
HEAD to read, and a consumer sorting raw files by stamp needs those to differ.
### Changed
- `README-DESC` no longer demands byte equality for the `org-profile` class,
via `readme_desc_match: false` on the class — data in the register, not a
hardcoded name. For an ordinary repo the README opening and the forge
description describe the same subject and equality is right; for this one
class they do not — the README is the organisation's landing page, the forge
text describes the repo. Both are correct about their own subject, so it is
the equality that does not apply. A landing page's opening line also carries a
markdown link while the description field renders as plain text, so passing
the old check would have meant putting raw markdown on a real surface. The
class has one member, so coverage is lost nowhere else, and the exemption is
still RECORDED as an `OK` finding naming the reason rather than dropped.
Measured: `.profile` went from `ERROR` to 0 ERROR / 0 WARN, and the same
README under a `plugin` class is still an `ERROR`.
## [0.5.0] — 2026-08-09
### Added
- `LINK-INTERNAL-FIXTURE`: a dead link inside a `test/`, `tests/`,
`fixtures/`, or `*golden*` path now reports as `SKIP` instead of `WARN`.
These paths are presumed to break their own links on purpose —
`nav-golden-escape/bundle/index.md`'s deliberate `../../../../etc/passwd`
escape pops the whole base path instead of resolving outside the repo, so
it read as a genuine dead link against three repos in the org. The finding
still fires with file and line; only the judgement changes. Measured
before shipping: 16 `LINK-INTERNAL-*` findings before, 16 after, across
all 20 local clones — every one converted 1:1, none disappeared.
## [0.4.0] — 2026-08-09
### Added
- `LINKS-OPEN-REFS`: the `open/` reference check now reports its own result
instead of only its failures. Emitting nothing on success made "no dead
references" and "the check never ran" identical in the output — a sweep
across the org could not tell 19 clean repos from 19 unread ones (measured:
org-ops census 03b). The OK line carries the count it resolved, because an
OK that cannot say how many it checked is the same silence in a different
colour. Zero enumerated files is now `LINKS-OPEN-REFS-UNAVAILABLE` (SKIP),
which is the honest name for the case that used to look like a pass.
- `portfolio-optimiser-commons` registered as `shared-asset`, the org's 20th
repo. Class verified structurally rather than accepted: absent from the
catalog's `marketplace.json`, no `.claude-plugin/`, `README.md` + `LICENSE`
present, consumed by both reference implementations as a `git subtree`
so `vendor` is the right install form and `## Install` would be wrong to
require. Its absence was producing three false `LINK-DEAD` ERRORs in
`portfolio-optimiser` against a repo that exists, and leaving repo #20 with
zero coverage. Verified after the fact: `portfolio-optimiser` now has no
ERROR, and the commons repo measures as WARN with 9 checks passed.
### Fixed
- `LINK-DEAD`/`LINK-NON-REPO` false negative on a schemaless host. `URL_REF`
required `://` or `@host:`, so `git.fromaitochitta.com/open/<name>` — the
form a subtree instruction routinely uses — resolved to nothing at all
(measured: llm-security/V3-UPGRADE.md:343). What makes a name resolvable is
its position after a host, not the scheme in front of it. Two guards keep
the widening from becoming noise: the host must end in a TLD-shaped label,
and a candidate preceded by `/` is a path segment that merely contains a
dot, not a host — so `docs/v1.2/open/` and `test/nav.golden/open/` stay
silent, and the API-endpoint rule from 0.3.0 is untouched. Measured before
shipping across 1501 tracked Markdown files in 20 local clones: 14 lines
changed verdict, 13 of them references that were previously invisible.
- The 14th was a defect the widening introduced, caught by that same
measurement and fixed here: a markdown link whose display text repeats its
own URL matched on both halves, which prints one dead reference twice and
inflates the count the new OK line offers as evidence. References are now
deduplicated per name-and-line — two different names on one line, or the
same name on two lines, still count as two.
## [0.3.0] — 2026-08-04
### Added
- `BADGE-DEAD-LINK`: a linked badge making a run claim ("tests", "build",
"CI", "passing") is now resolved against the repo's tracked files when the
link target is relative. Being wrapped in a link previously ended all
scrutiny — the link-target capture group was missing from the regex
outright, so a badge linked to a dead relative path passed silently.
External targets (the ordinary case — a CI provider) still need the
network and stay out of scope, same precedent as `checkInternalLinks`.
### Fixed
- `LINK-DEAD` false positive on API endpoint paths: `URL_REF` matched
`open/<name>` anywhere in a URL, so a Forgejo API call like
`.../api/v1/orgs/open/repos` read as a dead reference to a repo named
"repos". `open` must now be the first path segment after the host,
matching how every real repo URL is shaped. Measured twice against the
catalog's own `RUNBOOK.md`.
- 429 backoff was tuned for a hard IP ban that does not exist. Measured
directly against the live forge: nginx never sends `Retry-After` (the
branch handling it is dead code in practice), the limit is a leaky bucket
that can take up to ~15s to drain, and it is IP-based rather than
token-quota-based — a valid token made no difference. `fetchWithRetry` now
defaults to a 23s worst-case budget (`retries: 5`, `maxDelayMs: 8000`), up
from 7s. `CLAUDE.md`'s "13 calls in a loop" explanation corrected to match.
- Two tests asserted facts about their own local test fixture instead of the
real `register/repos.json` — a typo in the real register would have
drifted past them silently. Both now read the live register via
`loadRegister()`.
## [0.2.2] — 2026-08-04
### Fixed

270
CLAUDE.md
View file

@ -28,47 +28,7 @@ would recreate, in data, exactly the drift this plugin exists to remove.
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 — and never a severity either.** A check that could
not run says so and names why. But a skip is the ABSENCE of a verdict, so it
cannot be the worst of a set that contains real ones: a repo's `status` is the
worst *judged* finding, and `SKIP` only when nothing was judged (unregistered,
or an empty finding set). It used to outrank `OK`, which made five repos with
0 ERROR, 0 WARN and a dozen OK headline as "skipped" in org-ops census 05 —
`okr`, with the most OK in the org, read as unread. Fixing it in the status
alone would have traded that for the worse direction, "skipped checks look
clean", so coverage rides its own axis: `notChecked` in `--json`, a `· N not
checked` qualifier on the summary line. Absent `notChecked` means an older
engine and prints the pre-0.7.0 line — it is not zero.
- **A skip is one of TWO facts, and merging them says the same wrong thing more
quietly.** `byDesign` (the check saw it and declined — out-of-repo links,
fixture paths; it can never become a verdict, nobody has an action) versus
`notRun` (a re-run or an operator action resolves it). Measured across 19
clones, 35 of 39 skips were the first kind, which is why `portfolio-optimiser
— OK · 11 not checked` read as eleven unread checks. The kind is set at the
EMISSION site, never looked up from the code: `VERSION-TAG` is emitted at
`SKIP` with no tags and at `ERROR` with the wrong one, so a code→kind map has
to re-derive a reason the emission site already had. Untagged falls to
`notRun`, the loud side. The summary line names only `notRun`; the deliberate
ones keep their own `NOT JUDGED` heading, because the wanted side effect is
exposure, not silence — landing the split in `--json` alone would have left
the symptom untouched. The test that holds this is a scan of the engine
SOURCE, not of a finding set: a corpus test only sees the sites it manages to
trigger, and the next site added would take the reader's default invisibly.
- **"Nothing to judge" is an `OK`, not a skip — and it is a THIRD shape, not a
third bucket.** A check that ran, saw everything, and found no subject has
produced a verdict: there is nothing here to be wrong. `checkReadmeLanguage`
already answered that way for a README with no prose; `VERSION-NONE` (0.9.0,
was `VERSION-UNAVAILABLE`/`notRun`) does for a repo that claims no version.
The deferral that kept it a skip for one release was recorded in the engine as
"re-levelling moves a repo's status" — measured false: an added `OK` cannot
worsen the worst *judged* finding, and all three affected repos already read
`OK`. Status moves only for a repo whose *entire* finding set was skips. The
lesson is not about this code: **a recorded decision that is wrong is worse
than no record**, because the next session re-derives the measurement instead
of reading it. And the guard against `OK` blessing a real gap is that the gap
is caught elsewhere — no class requires a version file, and a `plugin` missing
its manifest is an independent `FILE-MISSING` `ERROR`. Without that, this
would have been "SKIP is never a pass" one level up.
- **`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
@ -88,115 +48,19 @@ would recreate, in data, exactly the drift this plugin exists to remove.
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.
- **A dead pin is not a dead link, and it is not a wrong manifest.**
`PIN-DEAD` asks whether the ref a README install command pins actually
resolves — `LINK-DEAD` asks whether the repo exists, `VERSION-TAG` asks
whether the MANIFEST's version was tagged. All three coincided on guard only
because one wrong number was written in three places. It is an `ERROR`
because a dead documentation link costs a stranger a 404 while a dead pin
costs them the install. Resolved against the FORGE, never the clone: a local
tag can exist unpushed, which portfolio-optimiser demonstrates. A pin at a
branch or a sha is a `byDesign` skip — `ls-remote --tags` cannot answer it,
and looseness is a different finding from deadness.
- **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.
- **Three API calls per invocation, anonymous, with 429 retry.** The org listing
- **Two API calls per invocation, anonymous, with 429 retry.** The org listing
(description + topics) is one; the catalog's `marketplace.json` for
INSTALL-TRUTH is the second (added after this used to say "one call" — that
INSTALL-TRUTH is the other (added after this used to say "one call" — that
line went stale and stayed stale until a 13-repo shell loop trusted it and
tripped the rate limiter at 26 requests); the repo's releases listing for
`RELEASE-CURRENT` is the third, and is the only subject the cheaper git
channel cannot cover, because a release is not a ref. This count is the line
that has now gone stale twice — it is updated in the SAME commit as the check
that changes it, never afterwards. All go through `fetchWithRetry`,
which retries HTTP 429 rather than silently reporting SKIP. All are
anonymous — no token, confirmed no different with one — so the gate works
for any reader, not only someone holding one. A sweep across every repo
still does not belong here: it needs the listing fetched once, not once per
invocation, which is a different shape of caller (org-ops), not a flag on
this engine. **The arithmetic moved with the count**: a full online sweep of
22 repos is now 66 API calls, not 44. Nobody has measured 66, and the only
knee ever measured is the ~40 marked unusable below. A sweeping caller paces
or fetches once — this sentence exists so the number is not re-derived from a
stale "two calls", which is how that line failed twice.
**The "13 calls in a loop" explanation was incomplete** (2026-08-04): the
forge's nginx never sends `Retry-After` on its 429s (measured directly), so
`fetchWithRetry` always falls back to exponential backoff — the
`Retry-After` branch is live code with no live path yet. The limit is also
smaller than "loop of 13" implied: 20 concurrent requests from one IP
reproduced it directly, no loop needed, and a single well-formed 2-call
invocation can still lose if something else on the same IP is calling the
forge at the same moment (other repos' hooks, another session). The block
is a leaky bucket, not a fixed ban — a 20-25 request burst took up to ~15s
to fully drain. `fetchWithRetry` defaults to `retries: 5` /
`maxDelayMs: 8000` (23s worst case) to cover that.
**Burst is not the only way in** (2026-08-12): a *steady* 1 request/second
tripped 429 at around request 40, so pacing alone does not buy immunity —
only a smaller total does. Recovery measured at 35s drain plus 3s pacing:
12/12 through. **That ~40 is unmarked and cannot be quoted as a forge
number**: the run did not record which endpoint class it hit, and org-ops
measured ~110 sequential `/raw/branch/main/` reads at 0.4s with zero 429 the
same day. Either they are two different limiters — the likelier reading —
or the ~40 is wrong, and nothing in the note distinguishes those. The one
API-axis figure this repo can stand behind: 22 sequential `/api/v1/` calls
at 2s pacing, anonymous, zero 429 (the `RELEASE-CURRENT` baseline run). That
is below the suspected knee, so it bounds nothing — it is a floor, not a
ceiling. Every future measurement records its endpoint class.
- **Two acquisition channels, and refs are not one of the API calls.** Four
queued checks need state the local clone does not have, and answering the
mechanism question once is the point — asked per check, it gets four
inconsistent answers and the budget grows by accretion. Anything answerable
from *refs* goes over the git protocol: `git ls-remote --tags` is anonymous,
reports annotated-vs-lightweight for free (the peeled `^{}` ref), and does
**not** share the API's bucket — 20 rapid calls all returned, and an API call
immediately after still got 200. Measured sequentially and only to 20, so it
is "does not consume the API budget", not "unlimited". Releases have no git
equivalent, so `RELEASE-CURRENT` is the third API call and the only new one.
**Derive the URL from the register (`forge` + `org` + name), never from
`origin`.** `app-creator`'s origin is `ssh://git@…`, which needs the
operator's key — it would work on this machine and fail for every other
reader, which is the anonymity invariant failing silently rather than loudly.
Signature status splits the same way: `git cat-file tag` reads the signature
block locally, while the forge's "Verified" badge needs a key registered
there — so `TAG-SIGNED` answers *is it signed*, never *does the forge vouch
for it*.
- **A stale release page is a WARN, and "no releases" is not a finding at all.**
`RELEASE-CURRENT` compares the newest release the forge publishes against the
newest tag the forge lists — both sides from the FORGE, never the clone.
Comparing a local tag against a published release would report
portfolio-optimiser as stale when the real defect is a tag that was never
pushed (v1.0.0 local, v0.1.0 published) — REMOTE-SYNC's subject, not this
one. Measured across all 22 registered repos (2026-08-12): 4 have no tags,
2 tag without ever publishing a release, 11 are current, 5 lag. Those 2 —
`ktg-plugin-marketplace` and `llm-security-commons` — are why zero releases
is an `OK` (`RELEASE-TAGS-ONLY`) and not a finding: nothing in a repo says
which of the two legitimate conventions it follows, which is the measurement
that rejected `VERSION-DRIFT` one check over. Lagging is a `WARN` because
the remedy is safe — publishing a release for a tag that already exists
moves no published ref, unlike the remedy `TAG-ANNOTATED` has to withhold.
It is also `WARN` because this repo is the worst offender in the org
(v0.9.0 tagged, v0.3.0 published), and an `ERROR` would have let the gate
decide an operator question — whether this org publishes releases at all —
by exiting 1 on its own author.
- **An unpushed tag is a version that exists for nobody, and it is
`VERSION-TAG`'s blind spot, not its duplicate.** `VERSION-TAG` reads LOCAL
tags, so a manifest claiming `1.0.0` against an unpushed `v1.0.0` reads as a
clean pass — `portfolio-optimiser` read `OK` until `REMOTE-SYNC` existed.
One subject in the corpus is what got `BRANCH-STALE` rejected; the difference
is that an unpushed tag is never one of two legitimate conventions the way
tag-only releasing is, the remedy (`git push origin <tag>`) moves no published
ref, and the finding recurs at every release rather than once. The reverse
direction — a clone behind the forge — is deliberately not a finding: that is
the reader's machine state, not the repository's, and failing correct repos on
it is how gates get switched off.
- **Version order, in the measurement as well as in the code.** The shell that
measured this check's baseline sorted tags with `sort -t. -k1,1V` and put
`v0.9.0` above `v0.10.0` — the exact defect `compareTags` already exists to
fix, reproduced in the tool that was supposed to validate it. It named
`repo-mailbox`'s newest tag as v0.9.0 when it is v0.21.0. A baseline derived
with different ordering than the rule cannot validate the rule. Derive
"newest" with the engine's own comparator, or the measurement is fiction.
tripped the rate limiter at 26 requests). Both go through `fetchWithRetry`,
which honors `Retry-After` on HTTP 429 rather than silently reporting SKIP.
Both are anonymous — no token — so the gate works for any reader, not only
someone holding one. A sweep across every repo still does not belong here:
it needs the listing fetched once, not once per invocation, which is a
different shape of caller (org-ops), not a flag on this engine.
- **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
@ -204,121 +68,16 @@ would recreate, in data, exactly the drift this plugin exists to remove.
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 fixture-path dead link is `SKIP`, not `WARN` — and never silently
dropped.** `test/`, `tests/`, `fixtures/` (exact segment) and `*golden*`
(substring) mark a path as presumed intentional; the finding still fires as
`LINK-INTERNAL-FIXTURE` with its file and line, it just isn't judged.
Grounded in `nav-golden-escape/bundle/index.md`'s deliberate
`../../../../etc/passwd` escape: the deep `..` pops the whole base path
rather than resolving to `null`, so it read as a genuine `WARN` — third tool
in the org to hit this exact pattern, which is the signal the check was at
fault. Measured before shipping: 16 findings before, 16 after, across all 20
local clones — every one converted 1:1, none disappeared.
- **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.
- **A decision needs somewhere to live, or the gate repeats itself forever.**
The engine already held that a human README title is the operator's call —
and still warned about it every round, because a YES could not be recorded.
Six warnings, unchanged across censuses 05, 06 and 07. `titles` in the
register is that record: set, the H1 matching it is `OK`; unset, the `WARN`
stands. What the gate must never do is make "we decided this" and "nobody
looked" the same output. The wanted side effect is exposure, not silence —
`ai-psychosis` is deliberately unregistered so it stands alone.
- **An exemption is a finding, not a deletion.** `readme_desc_match: false`
turns off README-DESC equality for a class, and the check still emits an `OK`
naming why. An exception nobody can see reads exactly like a check that
silently stopped running.
- **Class rules live in the register, never as a class name in the engine.**
The `org-profile` exemption is a flag on the class, not `if (klass ===
'org-profile')`. Per-repo copies of a rule are the drift this plugin exists
to remove; a class name hardcoded in a classifier is the same defect one
level up.
- **The version names a file; only the sha names the code.** `engineVersion`
was added because a stale cache served an old engine silently — but a feature
and its version bump are two commits, so a worktree carries new behaviour
under the old number for a window. Measured: a sweep stamped 18 raw files
`0.4.0`, four of them holding findings from a check that only exists in
`0.5.0`. `engineCommit` closes that, derived from the same checkout with no
network call. It is present-and-`null` when underivable, never absent — an
absent key means an older engine, `null` means this one ran without a HEAD.
- **A finding must name a remedy the operator can safely perform.** A
lightweight tag is movable without a trace, and the catalog pins plugins by
tag — so it is a supply-chain property, not tidiness. But the levels come
from a measurement: 155 tags across 19 clones, 14 lightweight, and only ONE
repo whose *newest* tag is lightweight. The newest can be re-cut at no cost
(`ERROR`); the older ones can only be "fixed" by force-moving an already
published ref — the very act the check warns about — so they are exposed
once, as a count (`WARN`), never as fourteen findings. Left unrecorded, that
`WARN` can never be cleared, which is the `titles` problem again; no
acceptance record is built until a repo actually needs one.
- **`VERSION-DRIFT` was approved, measured, and not built.** The proposed rule
— the manifest carries a released version string while HEAD sits N commits
past that tag — fires on **15 of the 18 tagged repos**, twelve of which are
simply between releases (ms-ai-architect 131 commits, the catalog 262). A
gate that fails a correct repository is the mechanism that gets gates
switched off, and this one fails almost all of them. The two repos that
motivated it are both already answered: guard's manifest claims `0.7.0` with
no such tag, which is an existing `VERSION-TAG` `ERROR`; okf's case
turns on *behaviour-changing* commits past the tag, which no classifier reads
off git. Two legitimate conventions coexist here — bump-at-release and
bump-first — and nothing in a clone says which one a repo follows.
This says nothing about org-ops' `PIN-DEAD`, which is a different check on a
different subject: `VERSION-TAG` reads the MANIFEST and asks whether that
version was ever tagged, `PIN-DEAD` reads a README INSTALL COMMAND and asks
whether the ref it pins resolves. They coincide on guard only because the
same wrong number was written in both places; a README pinning a bad ref in a
repo with a correct manifest is invisible to `VERSION-TAG`. Recorded
rather than deferred: a decision that is wrong is worse than no record, and
the next session should not re-derive this measurement.
- **`RELEASE-ASSETS` is rejected permanently, and the reason is NO SUBJECT — not
the fire rate.** All 18 releases in the org carry no assets, but that number is
the weaker half of the measurement. The other half: **0 of 21 READMEs mention
an asset download at all** — no `releases/download` link, no tarball, nothing.
Every install path in this org goes through `marketplace add`, `plugin
install`, `pip`/`uv`, or a clone. So there is no reader who is harmed and no
remedy anyone would perform. That is a stronger rejection than VERSION-DRIFT's,
which was "fires on repos following the other legitimate convention"; this one
has no convention on either side because it has no subject. Do not re-derive
it from "18/18" alone — the fire rate is a proxy, and reasoning from the proxy
is what would resurrect this check the first time one repo publishes an asset.
- **`TAG-SIGNED` is BLOCKED ON AN OPERATOR DECISION, not rejected — and filing it
with the rejections is the error to avoid.** 0 of 18 repos sign their tags
today, which looks like the same measurement that killed VERSION-DRIFT and is
not: if the operator decides this org signs tags, the check acquires its whole
subject the same day. A rejection is permanent; this is one yes/no away.
Recorded here so the next session reads a pending question instead of
re-measuring a settled one. The scope is also fixed in advance: `git cat-file
tag` reads the signature block from the clone, so the check can answer *is it
signed* and must never claim *the forge vouches for it* — the forge's
"Verified" badge needs a key registered there.
- **A gate whose subject varies per repo MEASURES the subject; it does not read
it off the class.** `VERIFY-COMMAND` fires only where something runnable
exists (`scripts.test`, or a tracked executable test file), and answers
`VERIFY-NONE` at `OK` where nothing does. The five subject-less repos —
human-friendly-style, llm-security-commons, playground-design-system,
portfolio-optimiser-commons, app-creator — span `plugin`, `shared-asset` AND
`standalone`, so every class-level phrasing of this rule fails a correct
repository somewhere. The rate is fine (6 of 21 `WARN`) for a reason that must
travel with the rule: the org publishes exactly ONE stance on verification
("no CI badge, so one command from a clean clone"), so an undocumented suite
is not a second convention the way bump-first vs bump-at-release was for
VERSION-DRIFT. And the check runs nothing, so its `OK` says *documented*,
never *passing* — a green line implying a passing suite would be a claim on
the surface that nobody verified.
- **The check table in the README is part of the surface it measures.** Four
checks shipped without a row (`PIN-DEAD`, `TAG-ANNOTATED`, `RELEASE-CURRENT`,
`REMOTE-SYNC`), and the Requirements section still said "two network calls"
after the third was added — the same stale-count defect the invariant above
warns about, one file over, on the README of the repo whose entire job is
presentation. A new check is not shipped until its row exists.
- **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 # 230 tests
npm test # 103 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
@ -327,15 +86,6 @@ node scripts/repo-standard-check.mjs --refresh # register vs. forge
## Release
**Run `--refresh` before every release.** Register freshness is owned HERE, not
by the sweeps that read the register. Twice running, a newly published repo was
missing when a census ran, and the cost is not a gap — it is false ERRORs in a
*different* repo: `portfolio-optimiser` earned three `LINK-DEAD`s against a repo
that existed, in the same round it fixed its three real ones, so its status line
did not move even though the work was done. A stale register makes the gate
lie about repos that are not even the stale one. One owner, no shared duty:
consumers of the register are told not to check freshness themselves.
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`.

View file

@ -1,131 +0,0 @@
# Governance
How the repositories in this organisation are maintained, what you can expect from upstream, and how they're meant to be used.
## TL;DR
- Solo-maintained, AI-assisted development, MIT licensed.
- **Fork-and-own is the default model.** Upstream is a starting point, not a vendor.
- Issues welcome as signals. Pull requests are not accepted — see [Why no PRs](#pull-requests--no).
- No SLA. Best-effort bug fixes and security advisories. Breaking changes happen and are noted in each repository's CHANGELOG.
---
## Can I trust this?
Be honest with yourself about what you're adopting:
- **One maintainer.** If I get hit by a bus, the bus wins. The repos stay up under MIT, but no one owes you a fix.
- **AI-generated code with human review.** Everything here is built through dialog-driven development with Claude Code. I read, test, and judge the output before it ships, but I'm not auditing every line the way a security firm would. Treat it accordingly.
- **No commercial interests.** I'm not selling a SaaS, not steering you toward a paid tier, not collecting telemetry. Nothing here runs as a hosted service; code runs locally where you install it.
- **MIT licensed.** Fork it, modify it, ship it under your own name.
If you work somewhere that needs vendor accountability, support contracts, or signed assurances — **this isn't that.** Use it as a reference implementation, fork it into your own organization, and own the result.
---
## How this is meant to be used
### Fork-and-own
The intended workflow:
1. **Fork** the repository you need into your own organization or namespace.
2. **Tailor** it to your context — terminology, integrations, cycle lengths, regulatory framing, whatever doesn't fit out of the box.
3. **Maintain it yourself.** Treat your fork as the canonical version for your team.
4. **Watch upstream selectively.** Cherry-pick changes that help, ignore changes that don't. There's no obligation to stay in sync.
This isn't a workaround for not accepting PRs. It's the actual recommended adoption pattern, especially for plugins like `okr` and `ms-ai-architect` where every Norwegian public sector organization will need its own tildelingsbrev mappings, terminology, and integrations. A central "one true version" would be wrong for everyone.
### What to change first when you fork
Each repository differs, but the common edits are:
- **Identity** — rename, replace authorship, update README.
- **External integrations** — issue trackers, knowledge bases, dashboards, observability backends. Everything ships as a starting point, not pre-wired. Every organization must configure its own integrations.
- **Norwegian-specific framing** — relevant for `okr` and `ms-ai-architect`. Most repositories are jurisdiction-neutral. Rewrite for your jurisdiction if you're outside Norway.
- **Reference docs** — the knowledge bases reflect my reading. Replace with your organization's authoritative sources.
- **Hooks and policies** — security thresholds, blocked commands, and audit gates are tuned to my taste. Tune them to yours.
### Staying current with upstream
If you want to pull in upstream changes later:
- **Cherry-pick, don't merge.** Each repository moves independently and breaking changes land without ceremony.
- **Read the CHANGELOG first.** Every repository has one.
- **Keep your customizations in clearly-named files.** The harder upstream is to merge cleanly, the more painful staying current becomes. A `local/` directory or `*.local.md` convention helps.
---
## What upstream provides
| | What I do | What I don't |
|---|---|---|
| **Bug fixes** | Best-effort when I notice or get a clear report | No SLA, no triage commitment |
| **Security issues** | Investigate within reasonable time, document in CHANGELOG | No CVE process, no embargo coordination |
| **New features** | When they fit my own usage | Not on request |
| **Norwegian public sector context** | Kept current as long as the project lives | If I lose interest or change jobs, the framing freezes |
| **Breaking changes** | Documented in CHANGELOG | They happen — version pin if you need stability |
| **Compatibility** | Tracked against current Claude Code releases where relevant | No long-term support branches |
If any of this is a dealbreaker — fork now, version-pin, and stop reading upstream.
---
## How to contribute
### Issues — yes, please
Issues are the most valuable thing you can send me:
- **Bug reports** with reproduction steps. Even a screenshot helps.
- **Use-case feedback.** "I tried to use this in my organization and X didn't fit" is genuinely useful, even if I can't fix it for you.
- **Pointers to better sources.** If you know a DFØ veileder, an NSM guideline, or an academic paper that contradicts what's in a knowledge base, tell me.
- **Security findings.** See the repository's `SECURITY.md` for disclosure preference where one exists; otherwise email rather than open a public issue.
### Pull requests — no
This is deliberate, not laziness:
- **Solo review is a bottleneck.** Honest PR review takes me longer than rewriting from scratch. The math doesn't work.
- **Forks are where the value is.** The fork-and-own model means upstream consolidation isn't the point. Your organization's adaptations belong in your fork, not mine.
- **AI-generated code complicates provenance.** Every line here is produced through dialog with Claude Code, with me as the judge. Mixing in PRs from contributors with different processes and licensing assumptions creates a mess I'd rather not untangle.
If you've built something useful on top of a fork, **publish it under your own name and link back.** I'll happily list notable forks here once they exist.
### Notable forks
*(To be populated as forks emerge. If you've forked one of these repositories for production use, open an issue and I'll add a link.)*
---
## Relationship between repositories
These repositories are **independent**. Install one without the others, fork one without the others. They share conventions (naming, hook patterns, AI-generated disclosure) but no runtime dependencies unless a repository's manifest declares one explicitly.
The plugin marketplace among them is a **catalog**, not a suite. Don't fork the whole thing unless you actually want to maintain everything.
---
## Versioning and stability
- **Semantic versioning per repository.** Each repository has its own `CHANGELOG.md` and version number.
- **Breaking changes happen.** I bump the major version when they do, but I don't run an LTS branch.
- **Pin your version.** If stability matters more than features, install a specific version and stay there until you choose to upgrade.
---
## Public sector adoption notes
For Norwegian etater specifically:
- **DPIA-relevant data flows are documented in the relevant README where applicable.** Read them before installation.
- **No data leaves your machine** beyond what Claude Code itself sends to Anthropic. Nothing here calls external services unless you configure an integration.
- **Drøftingsplikt and ledelsesansvar** are not replaced by these tools. The `okr` plugin coaches; it does not decide. The `ms-ai-architect` plugin advises; it does not approve.
- **Choose your Claude deployment carefully.** claude.ai vs. API direct vs. Bedrock in EU region have different data residency profiles. These tools don't choose for you.
---
## License
MIT is the default across the organisation. See each repository's `LICENSE` file for the authoritative terms.

151
README.md
View file

@ -6,11 +6,11 @@ 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 [GOVERNANCE.md](GOVERNANCE.md) for the full model.
> **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.10.1-blue)
![Version](https://img.shields.io/badge/version-0.2.2-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)
@ -41,17 +41,10 @@ a replacement for the two commands above:
## Requirements
Node 18 or newer. No dependencies. Three API calls: the org listing (for a
repo's published description), the catalog manifest (to confirm the install
command resolves), and the repo's releases listing (to see the version page a
stranger reads). Refs come over the git protocol instead — `git ls-remote
--tags`, which does not spend the API budget — and a release is not a ref, which
is why it is the one subject that needs a call of its own.
All of it reads anonymously, so no token is needed, and `--offline` skips every
one — 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.
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`.
## What it does
@ -68,7 +61,6 @@ The repository's **class** decides what each check means:
| 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 |
| Install pins | a `@v…` or `--branch v…` in an install command names a tag the forge does not have, so the command a stranger copies 404s |
| 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 |
@ -79,10 +71,6 @@ The repository's **class** decides what each check means:
| 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 |
| Tag integrity | the newest tag is lightweight — movable without a trace, under a catalog that pins by tag. Older ones are exposed once as a count, because their only "fix" is force-moving a published ref |
| Release current | the newest release the forge publishes is older than the newest tag it lists, so the version page shows a version older than the code. `WARN` — publishing a release for an existing tag moves nothing |
| Remote sync | a tag exists only in this clone. The manifest can read as correctly tagged while no stranger can resolve the version |
| Verify command | the repo has something runnable and the README shows no command to run it. With no CI badge in this org, that command is the only evidence a stranger gets |
| Description | empty, or past the length bound |
Findings carry two independent things: a **level** (`ERROR`, `WARN`, `SKIP`,
@ -92,51 +80,9 @@ Findings carry two independent things: a **level** (`ERROR`, `WARN`, `SKIP`,
- **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.10.0 @<sha>)
✗ some-plugin [plugin] — ERROR · 2 not run (repo-standard v0.10.0 @<sha>)
· stranger — SKIP · 1 not run (repo-standard v0.10.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.
The process exits 1 on any `ERROR`. A `SKIP` means the check could not run — an
unreachable forge, an untagged repo, a link that leaves the repository. It is
not a pass, and the output prints those separately under a heading that says so.
### Traits — a second axis
@ -158,63 +104,10 @@ 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
finding is a `SKIP` — the question is live and unanswered. 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.
### One command, because there is no CI
The forge has no Actions runners registered. A green badge here would assert a
run that never happens, which is the dishonest-badge finding one row up. The
substitute this project publishes is one command a stranger can run from a clean
clone, said plainly — so a repository with something runnable and no such command
in its README is a `WARN`.
Two things bound it. It fires only where a subject was **measured**: a
`scripts.test` entry, or a tracked file that is unambiguously an executable
test. Five of the twenty-one clones have neither — they hold prose, output styles
and domain packs — and they answer `VERIFY-NONE` at `OK`, because a check that
ran and found nothing to judge has produced a verdict. Those five span three
different classes, which is why the subject is measured and not read off a class.
And it can only ever fell a **missing** command. Nothing here runs anything, so a
green line means documented, never passing. Measured across the org: six
repositories have a suite their README never mentions.
### What is deliberately not required
`CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` and `MAINTAINERS.md` are required by no
@ -254,28 +147,6 @@ 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
@ -296,7 +167,7 @@ distinct from "an older engine that never tried".
npm test
```
170 tests over the pure classifiers. The reference fixtures are measured false
103 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

View file

@ -1,6 +1,6 @@
{
"name": "repo-standard",
"version": "0.10.1",
"version": "0.2.2",
"private": true,
"type": "module",
"engines": {

View file

@ -30,14 +30,11 @@
"repo-standard": "plugin",
"ktg-plugin-marketplace": "catalog",
"playground-design-system": "shared-asset",
"portfolio-optimiser-commons": "shared-asset",
"llm-security-commons": "shared-asset",
".profile": "org-profile",
"portfolio-optimiser": "standalone",
"portfolio-optimiser-claude": "standalone",
"llm-ingestion-pipeline-security": "standalone",
"llm-ingestion-okf": "standalone",
"app-creator": "standalone"
"llm-ingestion-okf": "standalone"
},
"$comment_non_repos": [
@ -72,18 +69,7 @@
"",
"Consumer-facing documentation is unaffected by working alone, and that is",
"the whole distinction: SECURITY.md, LICENSE, CHANGELOG, non-goals and honest",
"limitations exist for the reader, not for a contributor.",
"",
"`readme_desc_match: false` turns OFF the README-opening == forge-description",
"equality for a class. Default is on, and it is right for an ordinary repo:",
"both texts describe the SAME subject there. `org-profile` is the one class",
"where they do not — its README is the ORGANISATION's landing page and the",
"forge text describes the REPO. Both are correct about their own subject, so",
"it is the equality that does not apply, not either text. A landing page's",
"opening line also carries a markdown link, and the description field renders",
"as PLAIN TEXT: passing the check would mean putting raw markdown on a real",
"surface — trading a census ERROR for a visible defect. One member, so the",
"exemption removes coverage nowhere else."
"limitations exist for the reader, not for a contributor."
],
"classes": {
"plugin": {
@ -104,8 +90,7 @@
"org-profile": {
"required_files": ["README.md"],
"required_headings": [],
"install": "none",
"readme_desc_match": false
"install": "none"
},
"standalone": {
"required_files": ["README.md", "LICENSE"],
@ -114,36 +99,6 @@
}
},
"$comment_titles": [
"Where a decided YES about a README H1 gets written down. A human title is a",
"valid choice — `docs/rds-v1.md` prescribes `1. # Title` for every class, not",
"`# repo-name` — but until this map existed the decision had nowhere to live,",
"so census 05, 06 and 07 would have reported the same 6 WARNs forever.",
"",
"Set: the H1 is compared against it and a match is OK. Unset: WARN, exactly as",
"before. That makes 'we decided this is correct' and 'nobody has looked at it'",
"two different outcomes instead of one.",
"",
"PROPOSED BY THE OPERATOR, NOT MEASURED — like `traits` and `locales`, this is",
"a judgement. The five below are not one phenomenon: `.profile` CANNOT be",
"`# .profile` (the name is Forgejo's mechanism name for the org-profile repo),",
"`llm-ingestion-guard` is not a title at all but the published package name",
"(it is in `non_repos` above for the same reason), and the three plugins are",
"one consistent product convention. Each H1 was read from the repo, not",
"copied from a census.",
"",
"`ai-psychosis` (`# Interaction Awareness`) is DELIBERATELY ABSENT: it is the",
"one where a reader cannot connect the title to the repo. Leaving it standing",
"alone is the wanted effect of registering the others."
],
"titles": {
".profile": "fromaitochitta / open",
"llm-ingestion-pipeline-security": "llm-ingestion-guard",
"linkedin-studio": "LinkedIn Studio Plugin for Claude Code",
"llm-security": "LLM Security Plugin for Claude Code",
"ms-ai-architect": "AI Architect Plugin for Claude Code"
},
"$comment_traits": [
"A SECOND axis, orthogonal to class. Class is structural (read off the",
"catalog and the remotes); a trait is about what the code DOES, which no",
@ -186,34 +141,6 @@
"okr": "nb"
},
"$comment_tags_lightweight_accepted": [
"Where a decided YES about lightweight tag HISTORY gets written down — the",
"same job `titles` does for a README H1, one axis over. A lightweight tag is",
"movable without a trace and the catalog pins plugins by tag, so it is a",
"supply-chain property; but the only remedy for a PUBLISHED one is force-",
"moving it, which is the act the check warns about. Without this list the",
"WARN can never be cleared, and the gate says the same thing forever.",
"",
"Keyed on tag NAME, never on a count. A count stays satisfied the moment one",
"tag is re-cut and a different, unaccepted one takes its place.",
"",
"MEASURED 2026-08-12 from the LOCAL clones, not from the forge — which is",
"the very gap REMOTE-SYNC exists to name, so these are claims about three",
"OTHER repos and were sent to them by coord so a wrong name can be disputed.",
"",
"13 entries, not the 14 lightweight tags that exist. `ktg-plugin-marketplace",
"v7.7.2` is DELIBERATELY ABSENT: it is that repo's newest tag, the one",
"lightweight tag with a safe remedy (`git tag -a -f`), and it is an ERROR",
"today. Pre-accepting it would mean that cutting v7.8.0 instead of fixing it",
"makes the finding vanish silently — the cheap fix skipped and unrecorded.",
"If it ages into history un-remedied, a fresh WARN is the correct signal."
],
"tags_lightweight_accepted": {
"ktg-plugin-marketplace": ["v3.4.1", "v5.0.0", "v5.0.1", "v5.0.2", "v5.0.3", "v7.7.0", "v7.7.1"],
"llm-ingestion-okf": ["v0.2.0", "v0.3.0", "v0.3.1", "v0.3.2", "v0.4.0"],
"llm-ingestion-pipeline-security": ["v0.3.0"]
},
"description_max_codepoints": 180,
"$comment_length": [
"180 codepoints, not bytes and not UTF-16 units. The same string measures 248",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -25,39 +25,14 @@ the script cannot encode.
node "${CLAUDE_PLUGIN_ROOT}/scripts/repo-standard-check.mjs" --dir "$PWD"
Findings are `ERROR` (blocks), `WARN` (look, then decide), `SKIP` (not judged —
in one of two ways, see below), `OK`. Exit 1 on any ERROR. Add `--offline` to skip the three
API calls — the org listing, the catalog manifest, and the repo's releases —
`--json` for machine output, `--refresh` to compare the bundled register against
the live org listing. Refs are read over the git protocol instead and cost
nothing against that budget.
Findings are `ERROR` (blocks), `WARN` (look, then decide), `SKIP` (the check
could not run), `OK`. Exit 1 on any ERROR. Add `--offline` to skip the two
network calls, `--json` for machine output, `--refresh` to compare the bundled
register against the live org listing.
**Never report a `SKIP` as a pass.** A SKIP means the gate did not judge the
thing. Say which, and say which KIND — since 0.8.0 there are two, and they are
not interchangeable:
- **`NOT CHECKED` (`skips.notRun`)** — the gate could not see enough. An
unreachable forge or catalog, an unregistered repo, a repo with no tags. A
re-run or an operator action turns it into a verdict, so it belongs in your
report even when the status is green.
- **`NOT JUDGED` (`skips.byDesign`)** — the gate saw it and declined on
purpose. A link leaving the repository, a dead link inside a fixture path.
Nobody has an action. Reporting these as outstanding work is how a clean repo
gets rewritten for nothing.
The inverse error is reporting an `OK` as an un-run check. A check that ran and
found **no subject** is a verdict, not a skip: `VERSION-NONE` ("no version
claimed anywhere"), `VERIFY-NONE` ("nothing runnable, so no verification command
is owed") and `LANGUAGE` ("no running prose to judge") are `OK` and
carry no action. `VERSION-NONE` was `VERSION-UNAVAILABLE`/`notRun` before 0.9.0
— if you see the old code, the engine is stale; see the version note below.
That applies hardest when the repo's own status is green. Since 0.7.0 a `SKIP`
no longer outranks an `OK` in the status line, so a repo reads `OK · 3 not run`
rather than `SKIP`. The summary line carries only the `notRun` count — a repo
whose every skip was deliberate prints an unqualified verdict. **The line's
silence is not proof there was nothing to say:** read the `NOT JUDGED` block, or
`skips.byDesign` in `--json`, before you tell anyone a repo was fully judged.
**Never report a `SKIP` as a pass.** A SKIP means the gate could not see enough
to judge — an unreachable forge, an untagged repo, a link leaving the
repository. Say which.
**Confirm the version before you trust a green run.** Measured 2026-08-04:
`${CLAUDE_PLUGIN_ROOT}` resolved to a cached `0.1.1` in a live session while
@ -159,22 +134,6 @@ command that runs the whole suite from a clean clone, stated in the README along
with the fact that nothing runs it automatically. Verifiability is the goal; CI
is one mechanism for it, and it is not available here.
Since 0.10.0 that substitute is measured rather than assumed. `VERIFY-MISSING`
(`WARN`) means the repository has something runnable — a `scripts.test` entry, or
tracked executable test files — and its README shows no command in a code block.
Fixing it has exactly one honest move: **document the command that already
exists, after running it yourself.** Writing `npm test` into a README because
the finding mentioned it, without a `scripts.test` to back it, converts a `WARN`
about missing documentation into a false claim on the first screen — a worse
defect than the one you were sent to fix, and one this gate cannot catch,
because it never runs anything. A green `VERIFY-COMMAND` means *documented*,
never *passing*.
`VERIFY-NONE` needs no action at all. A repository of prose, output styles or
domain packs owes no command, and adding a hollow test script to clear a finding
that was never against it is the anti-pattern in its purest form.
## Diagrams — capability is not justification
Mermaid renders in a README on this forge. That was verified on the live