security -> SECURITY.md read as "missing required file for class
`standalone`", sending the operator to a class definition that never
listed the requirement. requirementsFor now carries each required
file's origin (class vs. trait) and checkRequiredFiles names whichever
actually required it. Failing test written first per the Iron Law.
No release forced by this alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WuWwACwhun35j52AnTxRKV
Gated on `findings.length === 0`, so a TAG-SIGNED-PREPOLICY OK silenced it —
and every repo in the org has pre-policy tags, which made the affirmative
verdict near-unreachable in practice.
Caught by dogfooding v0.11.0: this repo signed the first tag the check ever
judged, and the gate did not say so. Status was green either way, which is
exactly why it was worth fixing — a reader could not tell "signed its new tag"
from "has cut no tag since the policy". Two different facts wearing one
silence, which is the defect this engine already refuses to ship one check
over, where an exemption nobody can see reads like a check that stopped
running.
Keys the affirmative on the JUDGED findings only. Measured after: 18 repos
emit both codes, 3 NONE, still 0 ERROR / 0 WARN. 243 tests, from 241.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsPsVsvhrSaejK3cLPmnN2
The check sat parked as "blocked on an operator decision" on the strength of
0 of 18 repos signing. That zero was never evidence against it: it is what a
practice nobody has adopted yet looks like, and reasoning from it makes the
gate ratify the status quo it exists to move. The rule that settled it asks
something else — would a public AAA+ repository do this? Yes, and unlike the
two conventions that killed VERSION-DRIFT, there is no second convention under
which release tags are better left unattributable.
The measurement then decided the SCOPE, not the yes. Across 20 clones: 158 v*
tags, 144 annotated, 14 lightweight, 0 signed, and not one dated after
2026-08-12. A boolean "this org signs" would have failed 20 correct
repositories on day one with force-moving 144 published refs as its only
remedy — the mechanism that gets gates switched off. So the policy is a DATE in
the register (never in the engine, the rule that keeps the org-profile
exemption a flag): nothing fires today, teeth at each repo's next release.
Verified both directions, because a sweep returning zero proves nothing on its
own — a dead check returns zero too. 21 repos: 18 PREPOLICY + 3 NONE, 0 ERROR,
0 WARN; then against real git objects an unsigned post-policy newest tag does
fire ERROR.
Two exclusions keep one defect from being counted twice: a lightweight tag has
no object to carry a signature (byDesign skip naming TAG-ANNOTATED as owner),
and a pre-policy tag was correct when cut (OK, said out loud, never silence).
It answers "is it signed", never "does the forge vouch for it".
The README row ships in this commit, not after it, and the stale test count in
CLAUDE.md (230 → 241) is corrected here rather than left for the next reader.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsPsVsvhrSaejK3cLPmnN2
The forge has no Actions runners, so this org publishes no CI badge; the
stated substitute is one command runnable from a clean clone. A repo with
something runnable and no such command in its README is a WARN — and the
finding names what the repo already has, so the remedy is one line.
The subject is MEASURED, never read off a class. Five of 21 clones have
nothing runnable at all and answer VERIFY-NONE at OK; they span plugin,
shared-asset AND standalone, so every class-level phrasing of this rule
would fail a correct repository somewhere. Measured: 10 document a
command, 6 do not, 5 have no subject.
Two things bound the rule. It adds no API call, so it has no SKIP at all
— copying the null-input guard from every check since PIN-DEAD would
print a false "not run". And it runs nothing, so its OK says documented,
never passing.
Not built, with distinct reasons recorded as invariants: RELEASE-ASSETS
is rejected permanently for having NO SUBJECT (0 of 21 READMEs mention an
asset download; the 18/18 fire rate is a proxy and must not be quoted as
the reason). TAG-SIGNED is BLOCKED ON AN OPERATOR DECISION, not rejected
— filing it with the rejections would read as settled when it is one
yes/no from acquiring its whole subject.
Also fixes this repo's own surface, which had drifted behind its engine:
four checks had shipped without a row in the README check table, and
Requirements still said "two network calls" after the third was added.
230 tests (from 213).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwZeAZ8cHmGZofM9dryuT9
Two loose ends from the checks shipped today, both found by review rather than
by a failure.
`forgeTags` returns EVERY ref on purpose: PIN-DEAD has to resolve a pin written
as `config-audit/v5.0.0`, and filtering to `v*` would turn a live pin into a
false dead one. ktg-plugin-marketplace carries four such refs, so both new
checks see them. They parse to 0.0.0, so they can only read as behind a real
release, never as a newer tag one is lagging — and REMOTE-SYNC's unfiltered
side can only remove findings, never add one. Two tests pin that reasoning; the
remaining asymmetry is a coverage gap, not a false positive.
The API count went from two to three, so a full online sweep of 22 repos went
from 44 calls to 66. Nobody has measured 66, and the only knee ever measured is
the ~40 this same file now marks unusable. Written down beside the count,
because a downstream caller trusting a stale number is exactly how that line
failed twice.
213 tests (was 211).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWc4piM4QW6Jxfky2Rw4Z8
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 read OK until this check existed; it now reads ERROR, which
is the finding the gate was blind to rather than a new demand on the repo.
Measured across all 21 registered clones: exactly one 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 it
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.
211 tests (was 205).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWc4piM4QW6Jxfky2Rw4Z8
A release is not a ref, so `git ls-remote` cannot answer this one. It is
therefore the third API call per invocation, and the only new one the
acquisition model adds.
Both sides come from the forge, never the clone: comparing a local tag to a
published release would report portfolio-optimiser as stale when the real
defect is a tag that was never pushed — REMOTE-SYNC's subject, not this one.
Measured across all 22 registered repos before the rule was locked: 4 have no
tags, 2 tag without ever publishing a release, 11 are current, 5 lag. Those 2
are why zero releases is an OK and not a finding — nothing in a repo says which
of the two legitimate conventions it follows, the same measurement that
rejected VERSION-DRIFT. Lagging is a WARN because the remedy is safe, and
because this repo is the org's worst offender: an ERROR would have let the gate
settle an operator question by exiting 1 on its own author.
The shell that produced that baseline sorted tags lexically and put v0.9.0
above v0.10.0 — the exact defect compareTags exists to fix, reproduced in the
tool meant to validate it. Recorded as an invariant: derive "newest" with the
engine's own comparator or the measurement is fiction.
CLAUDE.md's API-call count moves from two to three in this same commit; that
line has now gone stale twice. The unmarked "429 at ~40" figure is marked
unusable — its endpoint class was never recorded, and org-ops measured ~110
raw reads at 0.4s with zero 429 the same day.
205 tests (was 196).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWc4piM4QW6Jxfky2Rw4Z8
Reported by org-ops (census 08, R1) and re-measured here against the FORGE
rather than taken on their word: 3 install pins in the org, 1 dead.
`llm-ingestion-pipeline-security` pins ITSELF to `@v0.7.0`; that tag does not
exist, newest is v0.6.1. Anyone copying the single install command out of that
README gets a hard pip failure.
ERROR, not WARN: a dead documentation link costs a stranger a 404, a dead pin
costs them the install.
Not a duplicate of two checks it sits near. `LINK-DEAD` asks whether the repo
exists; `VERSION-TAG` reads the MANIFEST and asks whether that version was ever
tagged. All three land on guard today only because the same wrong number got
written in three places — a README pinning a bad ref in a repo with a correct
manifest is invisible to both.
Resolved against the forge, never the clone: a local tag can exist without
having been pushed, which portfolio-optimiser demonstrates directly. That uses
this session's decided acquisition model — `git ls-remote --tags` on the
register-derived https URL, anonymous, no API budget, and only for the repos a
README actually pins (nothing at all for the 19 that pin none).
A pin at a branch or a sha is a `byDesign` skip. `ls-remote --tags` cannot
answer it, and a loose pin is a different finding from a dead one.
The corpus sweep found a defect a unit test had not: offline, guard emitted the
SKIP *and* an OK reading "1 install pin(s) resolve against the forge" — a pass
asserted for a pin nothing had read. SKIP is never a pass. The OK now counts
only what was actually verified ("N of M"), and two tests pin it.
Measured before and after across all 21 clones: purely additive, no existing
finding moved. 19 repos emit PINS-NONE (OK — the check read the whole README
and found no subject), 2 emit real pins. Online, okf's 2 pins resolve and
guard's 1 does not: exactly one new ERROR org-wide, matching org-ops.
196 tests (was 187).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AkHEqTSr1k3HbeiHu1ggW
Four queued checks (REMOTE-SYNC, PIN-DEAD, RELEASE-CURRENT, TAG-SIGNED) need
state a local clone does not have. STATE.md carried the mechanism as an open
question per check, which is how a two-call budget grows by accretion — asked
four times, answered four ways.
Decided once, from measurement:
- Refs go over the git protocol. `git ls-remote --tags` is anonymous, reports
annotated-vs-lightweight for free via the peeled `^{}` ref, and does not
share the API's rate-limit bucket: 20 rapid calls all returned, and an API
call immediately after still got 200. Stated as "does not consume the API
budget", not "unlimited" — the run was sequential and stopped at 20.
- Releases have no git equivalent, so RELEASE-CURRENT is the third API call
and the only new one.
- The URL comes from the register (`forge` + `org` + name), never `origin`.
`app-creator`'s origin is `ssh://git@…`; using it would work on this machine
and fail for every other reader — the anonymity invariant failing silently.
- TAG-SIGNED answers "is it signed" (`git cat-file tag`, local), never "does
the forge vouch for it" — the Verified badge needs a key registered there.
Also corrects the rate-limiter note: a steady 1 request/second tripped 429 at
around request 40, so the burst description was not the whole limit. Pacing
alone does not buy immunity; a smaller total does.
VERSION-DRIFT is recorded as approved, measured, and NOT built. The rule fires
on 15 of 18 tagged repos, twelve of them merely between releases. Its two
motivating cases are already answered elsewhere or need judgement no classifier
has. Recorded rather than left open, so the next session reads the measurement
instead of repeating it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AkHEqTSr1k3HbeiHu1ggW
Second of the approved §5 checks. A `file:///Users/ktg/...` link is dead
for every reader but its author, and it publishes that author's directory
layout on a surface meant for strangers.
The SCHEME is not the rule, and measuring first is why. Across the corpus
there are 40 such links, and they split 18/22: a documented convention
example (`[Brief](file:///Users/ktg/.../brief.html)` — the same two lines
copy-pasted into nine CLAUDE.md files) versus real machine paths. Firing
on `file:` would have been wrong 45% of the time on its first run, which
is the ratio that gets a gate switched off. A further 22 links use bare
placeholders (`file:///abs/path.html`) and are not leaks either.
The discriminator is not tuned to this corpus: `...` is not a path
segment, so a target containing `/.../` cannot resolve on ANY machine and
is by construction an illustration.
Level follows the established reader rule — root is the shop window
(ERROR), below it live session plans and agent working files (WARN).
Measured on the registered corpus: exactly one finding, guard's
CLAUDE.md:56, a real absolute path to a file that is IN the repo and
should have been linked as `docs/BRIEF.md`. Every one of the 18
illustrations stayed silent.
Honest limit: three repos holding 21 of the 22 real leaks
(from-ai-to-chitta, wiki-advise, claude-code-llm-wiki) are NOT in the
register, so no check runs against them at all. That is register
freshness (`--refresh`), not this check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lb7XmJGLnFSX9U7tgS7fKk
First of the approved §5 checks. A lightweight tag is a branch-like ref:
it can be moved to another commit with nothing recorded that it ever
pointed elsewhere. The catalog pins every plugin to `ref: vX.Y.Z`, so
this is a supply-chain property, not tidiness.
The two levels come from a measurement, not from taste. Across all 19
clones: 155 tags, 14 lightweight, but only ONE repo whose NEWEST tag is
lightweight. The newest is what a consumer resolves today and what an
operator can re-cut at no cost -> ERROR. The older ones can only be
"fixed" by force-moving an already published ref, which is the exact risk
the check exists to name -> exposed once as a count, WARN, never as
fourteen findings. A gate that demands an unsafe remedy gets switched off.
No tags at all is the VERSION-NONE shape: the check ran, saw every tag
there is, and found no subject. TAGS-NONE is an OK, not a skip.
Newest is decided by version order, not by the order git returns.
`git tag --list` sorts lexically, where v10.0.0 lands before v9.0.0 —
which would misjudge exactly the repos with the longest history
(repo-mailbox has 27 tags). Pinned in test.
Read from local git objects via `for-each-ref %(objecttype)` — zero
network, so the two-call budget is untouched.
Measured on the corpus, and it matches the census exactly: 1 ERROR
(ktg-plugin-marketplace v7.7.2), 3 WARN (catalog 7, okf 5, guard 1),
15 OK, 2 TAGS-NONE. No other repo moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lb7XmJGLnFSX9U7tgS7fKk
`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
`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
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
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: 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
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
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
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