`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
SKILL.md's opening list still glossed `SKIP` as "the check could not run" and
then contradicted itself five lines later, where 0.8.0 had added the two kinds.
A session reading only the list reports every deliberate skip as something that
failed to run, and sends someone to fix fixture links that are deliberate on
purpose. 0.7.1 exists because 0.7.0 shipped the same class of defect; this is
the check that was supposed to stop it happening twice.
README's `--offline` note and its bilingual-undecidable paragraph now name which
kind their skips are, and what resolves them. Both were true and incomplete —
incomplete in the one direction that matters now that the summary line names
only one of the two.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DHaQTcaD3w2C6PqVsAmrcv
`notChecked` merged "deliberately not judged" with "could not run". 0.7.0 took
SKIP out of the severity lattice, which fixed "clean repos look skipped"; this
fixes the same defect one level down, where it was saying so more quietly.
Measured across 19 clones, 35 of 39 skips are deliberate — so
`portfolio-optimiser — OK · 11 not checked` announced eleven unread checks when
all eleven were fixture and out-of-repo links.
Every SKIP finding now carries `skip: 'byDesign' | 'notRun'`, set at the
emission site rather than 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 has to
re-derive a reason the emitter already had. Untagged falls to `notRun`, the
loud side. Results carry `skips: { byDesign, notRun }`; `notChecked` stays a
number and stays the total, because a consumer doing `notChecked > 0` against
an object gets a silent false.
The summary line names only `notRun` — the deliberate ones keep their own
`NOT JUDGED` heading in the detail output. Landing the split in `--json` alone
would have left the symptom exactly where it was.
VERSION-UNAVAILABLE stays `notRun` by decision, not by default: it is arguably
"no subject to judge", the shape checkReadmeLanguage answers with OK, but
re-levelling it moves a repo's status and is a second behaviour change.
Verified: 169 tests. 19 clones swept with both engines offline — 220 judged
findings, every status, notChecked and buckets identical, no finding differing
beyond the new tag. The exhaustiveness test scans the engine SOURCE, not a
finding set: a corpus test only sees the sites it triggers, and the next
emission site added would take the reader's default invisibly.
BREAKING CHANGE: the summary line's coverage qualifier reads `· N not run` and
counts only un-runnable skips, where it read `· N not checked` over all of
them. `--json` gains `skips`; `notChecked` is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DHaQTcaD3w2C6PqVsAmrcv
Three published documents went out with 0.7.0 unread.
SKILL.md told a reader what a SKIP FINDING means and stopped there.
Complete before 0.7.0, not now: a repo can headline `OK · 3 not
checked`, and a session reporting the status word alone would report a
green pass over three unjudged findings. Status and count are now
required together. Shipping without this made the plugin whose purpose
is catching stale published documents ship one.
README's example output cited commits older than the behaviour they
demonstrate — 5884a64 (0.6.0) beside a `notChecked` that did not exist
in it, and a7276e6 two releases back. The version names a file; only the
sha names the code. Both now cite 10ad125.
README also now states the unit: `notChecked` counts un-judged FINDINGS,
not checks. portfolio-optimiser's `11 not checked` is almost entirely
LINK-OUTSIDE-REPO, so the bare number reads worse than the repo is. Named
as a known limitation, not corrected in the string — the fix is splitting
"deliberately not judged" from "could not run", which is the next release.
A repo's `status` is now 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: 0 ERROR, 0 WARN and a
dozen OK headlined as "skipped". Five repos in org-ops census 05, `okr`
among them with the most OK in the org, reading as unread.
"`SKIP` is never a pass" survives in the half of the rule that carries
it — an unregistered repo, or an empty finding set, still says SKIP,
because there is nothing else to be worst of.
Fixing the status alone would have traded "clean repos look skipped" for
"skipped checks look clean". So `notChecked` rides beside it: in --json,
and as a `· N not checked` qualifier on the summary line. Absent means an
older engine, not zero.
Measured across all 21 local clones from ONE saved sweep, so before and
after come from the same findings rather than two sweeps of a moving org:
343 findings before, 343 after. 8 repos moved, every one SKIP -> OK.
1 ERROR and 3 WARN before, 1 ERROR and 3 WARN after — the counts that
decide whether a repo needs work did not move.
BREAKING: consumers reading `.status` see a changed value domain.
147 -> 154 tests.
Three changes at the register/engine boundary, all agreed with org-ops
after census 05 and all about a check missing a place to record a
legitimate exception.
`titles`: an optional per-repo README title. Set, the H1 matches it and
is OK; unset, the WARN stands as before. A human title was already this
engine's stated position and rds-v1's prescription, but a decided YES had
nowhere to live, so the same 6 WARNs were reported three censuses running
and would have been reported forever. Five registered, each H1 read from
the repo rather than copied from the census; `ai-psychosis` deliberately
left out so the one repo where a reader cannot connect title to name
stands alone. Measured across 21 local clones: 6 WARN before, 1 after,
nothing else moved.
`readme_desc_match: false` on the org-profile class: for an ordinary repo
the README opening and the forge description describe the same subject
and equality is right; for this class they do not — the README is the
org's landing page, the forge text describes the repo. The equality is
what does not apply, not either text. Class data, not a hardcoded name,
and the exemption is RECORDED as an OK naming its reason, not dropped.
`.profile` went ERROR to 0 ERROR / 0 WARN; the same README under a plugin
class is still an ERROR.
`engineCommit`: the version names a file, only the sha names the code. A
sweep stamped 18 files 0.4.0 while four carried a 0.5.0-only finding —
feature and version bump are two commits, so the stamp lied without being
broken. Present-and-null when underivable, never absent: an absent key
means an older engine, null means this one ran without a HEAD to read.
135 to 147 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NELsvPY5gnJjN3esdhWYWC
A file living under test/, tests/, fixtures/, or a *golden* path is
presumed to break its own links on purpose. nav-golden-escape/bundle/
index.md's deliberate `../../../../etc/passwd` escape pops the whole
base path instead of resolving to null, so it read as a genuine WARN
against three repos in the org — the check was at fault, not them.
The finding still fires, as LINK-INTERNAL-FIXTURE at SKIP with file
and line, so it is never silently dropped. Measured before shipping:
16 LINK-INTERNAL-* findings before, 16 after, across all 20 local
clones — every one converted 1:1, none disappeared.
135 tests (was 129).
Same precedent as v0.2.0 and v0.3.0: a new check code is a minor bump,
not a patch. Bundles the register row for repo #20, the schemaless-host
fix, the reference dedup and the new OK/SKIP outcomes on checkLinks.
Also records the register-freshness decision in CLAUDE.md: --refresh
runs HERE before every release, and consumers of the register are told
not to check freshness themselves. Twice running a newly published repo
was missing when a census ran, and the cost landed in a DIFFERENT repo
than the stale one — portfolio-optimiser's status line stayed put
through a round in which it fixed everything really wrong with it.
Two owners of one invariant is the drift this register exists to remove.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxP9N3p1fG6UYSB8rATBL6
Same precedent as v0.2.0: a new check is a minor bump, not a patch.
Bundles this session's three fixes (LINK-DEAD false positive,
widened 429 backoff, BADGE-DEAD-LINK) and the tautological-test
correction.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
Missed updating this alongside the previous commit, which added 3
tests (113 -> 116) — same stale-count mistake this repo's history
already caught once (103 -> 111, ca81db5).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
Measured directly against the live forge: nginx never sends a
Retry-After header on its 429s (the branch handling it is dead code
in practice), the limit is a leaky bucket rather than a fixed ban (a
20-25 request burst took up to ~15s to drain), it is IP-based rather
than token-quota-based (a valid FORGEJO_TOKEN made no difference to a
reproduced burst), and it triggers well below "13 calls in a loop" —
20 concurrent anonymous requests reproduced it directly. The old
default (retries: 3, ~7s worst case) was tuned for a hard ban that
doesn't exist. fetchWithRetry now defaults to retries: 5 with a
maxDelayMs: 8000 cap (23s worst case), covering the measured drain
time without one attempt blocking for a full uncapped exponential
step. CLAUDE.md's explanation is corrected to match; test count in
README/CLAUDE.md updated for the two new tests (111 -> 113).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
npm test currently reports 111, not 103 — the number went stale as
tests were added. Caught by catalog's coord message after measuring
the v0.2.2 tag directly (git-archive extraction, not the worktree).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WB6C8cJLHo3PF2BLPVb5qt
Measured 2026-08-04: two repos (llm-security, config-audit) independently
proved the /repo-standard skill had resolved ${CLAUDE_PLUGIN_ROOT} to a
cached 0.1.1 while 0.2.0 was installed and the catalog pinned it — same
instruction, same variable, wrong engine. 0.1.1 has neither BADGE-COUNT nor
README-LANGUAGE, so a broadcast recommending the skill produced clean-looking
runs that could not have found what they were sent to find.
The header line and --json output now carry `repo-standard v<version>`
(headerLine/withEngineVersion, both pure and unit tested). SKILL.md tells the
reader to confirm it against the catalog pin before trusting green. The
stale-cache resolution itself is the harness's, not this repo's — not fixed
here, only made visible instead of silent.
TDD: 4 new tests. 111/111 green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01496ZWasKPnA627crFBXWhe
Measured 2026-08-04: 13 script invocations in one shell loop tripped an
anonymous Forgejo rate limit at 26 requests, because each invocation makes
TWO calls (org listing + catalog marketplace.json), not the ONE this repo's
own CLAUDE.md claimed. That line went stale when INSTALL-TRUTH added the
second call and nobody updated the count it depended on.
fetchWithRetry wraps both calls, honoring Retry-After on HTTP 429 instead of
silently falling back to SKIP. TDD: 5 new tests inject a fake fetch and sleep
to drive the retry/backoff/give-up paths without touching the network.
A sweep across every repo still doesn't belong in this engine — that's
org-ops's job by this file's own header — but a single repo's self-check
should not read as broken just because the forge was briefly busy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01496ZWasKPnA627crFBXWhe
BADGE-COUNT and README-LANGUAGE are additions, so this is a minor rather than a
patch. The catalog ref still sat on v0.1.1 against a released v0.1.3; bumping it
to v0.2.0 clears that lag in the same move, because a ref points, it does not
queue.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf1zhujv5QjYuAn1a9HcgW
Adversarial deep research (25 sources, 124 claims extracted, 25 verified:
11 confirmed / 14 refuted) plus direct measurement of all 18 cloned open/
repos. The useful half of the result is what it REFUSED to support, so the
research is recorded in docs/presentation-research-2026-08-03.md rather than
being spent and forgotten.
- BADGE-COUNT (WARN) — past five badges. Trockman et al., ICSE 2018
(n=294,941 npm packages) measured a non-linear relationship with popularity
inflecting at five, motivated by surveyed maintainers calling over-badged
READMEs cluttered and "trying too hard". WARN and never ERROR: the
coefficient sits in an appendix with no CI or p-value. Counting deliberately
uses a NARROWER rule than the existing claim check, so a screenshot or an
architecture diagram is never counted as clutter. Fires on 8 of 18.
- README-LANGUAGE (WARN) — prose not in the language this repo's readers were
declared to speak, via a new `locales` axis in the register. Class is
structural, a trait is what the code DOES, a locale is who it is FOR — the
standard's own "who the reader is decides what is required". English is the
default; ms-ai-architect and okr are declared nb, named by the operator as
Norway-only in audience. Stopword-frequency comparison over prose with code
stripped: a Norwegian flag name in a shell example cannot decide the
document. Fires on exactly those two, silent on all sixteen English repos.
One design correction found mid-implementation: the first version returned
SKIP when a README had too little prose to judge, which broke a passing
fixture and would have stopped any terse repo from ever reaching OK. SKIP is
for a check that could not RUN; this one ran, saw everything and found no
prose to be in the wrong language — the same shape as "no licence claim to
back". Insufficient prose is now OK, and evenly bilingual prose is the SKIP,
because there the question is live and unanswered.
Deliberately NOT built, because the evidence does not reach: any rule about
images, diagrams or terminal recordings (every such claim refuted 0-3); a
README length bound (no evidence-based target exists); a section count (would
fire on 9 of 18 — textbook "suspect the CHECK"); Mermaid source length and
#gh-dark-mode-only (zero occurrences, and the instance limit is not readable
via the API, so any threshold would be a guess).
Verified live against the operator's own forge (15.0.6+gitea-1.22.0): Mermaid
DOES render in README.md — two div.mermaid-block iframes carrying real SVG —
while #gh-dark-mode-only landed only in Gitea 1.26.0 and is unavailable here.
103 tests green, up from 92. No version bump: the catalog ref still trails at
v0.1.1 against 0.1.3, and starting a second release chain over that is the
operator's call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCAGKZT8h9F46ygzSkhEee
- extractChangelogTop truncated PEP 440 pre-release versions (0.5.0a2 -> 0.5.0),
so VERSION-CHANGELOG disagreed with VERSION-TAG over a version everything
already agreed on. Reported by llm-ingestion-okf with a repro.
- BADGE-STATIC-CLAIM treated a bare `status` badge as a run claim, same as
tests/build/CI. A self-declared maturity label asserts no run, same class
as version/licence/platform. Reported by llm-ingestion-pipeline-security.
- BOILERPLATE flagged FIXME when a scanner's own docs named the TODO/FIXME
convention rather than an instance of it. Reported by config-audit.
92 tests green, up from 86.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uwwfdmrfnp7FuGQ4z25RKH
`checkInternalLinks` compared a link's resolved target only against `present`
(tracked files), so `[x](dir/)` was always LINK-INTERNAL-MISSING even when
every file under it was tracked. Reported by portfolio-optimiser-claude with a
minimal repro; the same defect inflated ERROR counts in voyage, linkedin-studio
and portfolio-optimiser — 12 of the org's 71 measured ERRORs were this one
check, not twelve repo problems.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uwwfdmrfnp7FuGQ4z25RKH
The 0.1.0 entry said eleven checks; there are twelve, and both the code and
the README table already said so. 2963f4f fixed the entry in place but landed
after the tag, so the catalog's pin kept serving the wrong count.
Documentation only: scripts/ and register/ are unchanged from v0.1.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Wuy8mxQj8Zt4z23hiRjiN
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
Install truth is the brief's first control and the gate only checked
syntax. Now: the marketplace URL must be the real one (offline, from the
register), and the plugin must actually be pinned in the catalog (one
call, SKIP if unreachable). A well-formed `claude plugin install x@mkt`
fails silently when x was never pinned.
This makes the gate block ITSELF until publication finishes - the run
against this repo now has exactly one ERROR, and it is true: repo-standard
is not in the catalog yet. That is the post-publish acceptance test,
enforced mechanically instead of remembered.
Badge honesty no longer keys on img.shields.io. A self-hosted SVG asserts
the same unverified thing, and the README claimed the general rule while
the code checked one host.
Two stripCode bugs, both silent false passes:
- 4-space indent treated as code unconditionally made links inside nested
list items invisible. Fixed by requiring a blank line to OPEN a block.
- That fix alone ended the block after line 1, so multi-line indented
templates leaked back into scanning. Caught by the gate on this repo's
own SKILL.md, which shows a README template containing a CHANGELOG link.
A block now opens on a blank line and continues while the indent holds.
Also corrected two claims in this README: it said "one network call"
when there are two, and it still argued against a CONTRIBUTING using
reasoning the solo-maintainer section had already replaced.
77 tests. llm-security regression: still zero link and boilerplate noise.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h
Measured this build against a documentation brief for public repos. The
five original checks covered roughly one of its ten sections, so this
adds what a single repo can answer on its own.
New: required README headings per class (Non-goals is the cheapest
trust-builder there is), in-repo version consistency across manifest /
badge / CHANGELOG / tag, badge honesty, boilerplate, licence-claim,
and relative links. Findings now carry a BUCKET beside the level -
broken / missing / weakening - and output is grouped by it, because
that is the order the work gets done in.
Traits are a second axis beside class: class is structural and readable
off the catalog, a trait says what the code does. `security` attaches
SECURITY.md and a Known limitations section. The two names carrying it
are proposed, not measured - that list is the operator's.
Solo-maintained settles a category: CONTRIBUTING, CODE_OF_CONDUCT and
MAINTAINERS are required by no class. Consumer-facing documents are
untouched by that; SECURITY.md exists for the stranger who finds a hole.
Three bugs found by running against llm-security, not by reading:
- ~30 link findings, all noise. Regexes inside code spans are
`[...](...)` to a naive scanner. Strip code first.
- `file:` and other schemes were treated as repo-relative paths.
- Relative links were resolved against the repo root instead of the
file they sit in, calling two files missing that sat next to the
README linking them.
Same fix applied to the boilerplate check: a document ABOUT placeholder
detection was tripping the placeholder detector.
Also removed this repo's own static tests badge. There is no CI - the
forge has zero Actions runners registered - so it could never become
real, and it is the exact anti-pattern the gate now flags.
67 tests. Against llm-security every remaining finding is real and
matches the census's independent hand-measurement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h
The H1 change added two tests; the badge, the README line and the
CHANGELOG still said 32. Exactly the drift this repo exists to catch,
in this repo - and check-versions.mjs would not have caught it, since
it only reads the version badge.
Also states in the README what actually fails the first-screen check
now that a differing H1 is a WARN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h
Five checks a single repository can answer on its own: README first
screen, install block, files required by its class, open/<name>
references, description length. Pure classifiers with I/O resolved into
their input, mirroring check-versions.mjs; ERROR/WARN/SKIP/OK, exit 1
on ERROR. 32 tests.
The reference check has THREE outcomes: "matches no repo" (ERROR) is
separate from "matches a known non-repo" (WARN). Sharing an outcome
would let real dead links hide inside correct text. Only names in URL
position count, and .git is normalised first - without that a raw scan
turns 3 dead names into ~20.
enabledPlugins is treated as a legitimate second install form; what the
gate requires in addition is a CLI command. The JSON form is never
reported as the defect.
STATE.md is gitignored from this first commit - public remote.
No hook yet: a blocking gate must first be precise enough not to fail a
correct repository.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h