Compare commits

..

34 commits

Author SHA1 Message Date
f422b63bb3 fix(engine): the affirmative TAGS-SIGNED was suppressed by its own exemption OK
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
2026-08-13 10:54:35 +02:00
e6cd8983ca feat(engine): TAG-SIGNED — a tag that names a signer, not just write access
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
2026-08-13 10:51:36 +02:00
9f08ebc5ed chore(release): v0.10.1 — deliver the corrected skill
The catalog pins by tag, so 009c911's SKILL.md fix reaches nobody until a
tag carries it. An installed reader would still get "the two network
calls" and no VERIFY-COMMAND guidance from the release that introduced
the check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwZeAZ8cHmGZofM9dryuT9
2026-08-12 23:28:36 +02:00
009c911f9e docs(skill): the skill drifted behind the engine in the commit that banned it
v0.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
invariant was violated one file over, in the same commit that wrote it.

Three corrections, and one is a trap worth naming: the remedy for
VERIFY-MISSING is to 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. VERIFY-NONE needs no action at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwZeAZ8cHmGZofM9dryuT9
2026-08-12 23:28:06 +02:00
e00ed3340c feat(engine): VERIFY-COMMAND — the one command a stranger has instead of CI
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
2026-08-12 23:21:34 +02:00
0a4373c29b docs(invariants): the sweep arithmetic, and why forge refs stay unfiltered
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
2026-08-12 23:05:46 +02:00
aebd4d28a9 feat(engine): REMOTE-SYNC — a tag that exists only in one clone
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
2026-08-12 23:00:02 +02:00
a563035feb feat(engine): RELEASE-CURRENT — the version page a stranger actually reads
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
2026-08-12 22:57:35 +02:00
067ab0528d feat(engine): PIN-DEAD — the one command a stranger runs
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
2026-08-12 22:34:04 +02:00
425e7e9bdf docs(invariants): the forge-state acquisition model, decided once
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
2026-08-12 22:27:03 +02:00
fe0d039de2 feat(register): a decided YES about tag history, and app-creator registered
Two changes that belong together: both are the register learning to record a
decision the engine could otherwise only re-report forever.

`tags_lightweight_accepted` — TAG-ANNOTATED-HISTORY (WARN) fires on 13
historical lightweight tags in three repos. A lightweight tag is movable
without a trace and the catalog pins plugins by tag, so the finding is real.
But the only remedy for a published one is force-moving it — the exact act the
check warns about — so the WARN could never be cleared. That is the `titles`
defect one axis over: the gate could not tell "we decided this" from "nobody
looked".

Keyed on tag NAME, never a count: a count stays satisfied the moment one tag is
re-cut and a different, unaccepted one takes its place.

An accepted tag emits `TAG-ANNOTATED-ACCEPTED` at OK naming the tags — it is
not dropped. An exemption is a finding, the rule `readme_desc_match` already
follows; an exception nobody can see reads exactly like a check that silently
stopped running.

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, and an ERROR today. Pre-accepting it would
mean cutting v7.8.0 instead of fixing it makes the finding vanish silently.
The engine enforces this independently — the newest tag cannot be accepted
away even if named, and a test pins that.

`app-creator` → `standalone` — `--refresh` measured 22 on the forge against 21
registered. Unregistered meant zero checks against a repo published on `open/`.
The class is derived, not guessed: no `.claude-plugin/plugin.json`, absent from
the catalog, own remote on `open/` — identical in form to the four existing
`standalone`.

Measured before and after across all 21 clones, every finding code diffed:
- TAG-ANNOTATED-HISTORY 3 WARN -> 0, converted 1:1 to 3 OK. None disappeared.
- TAG-ANNOTATED unchanged at 1 ERROR (ktg-plugin-marketplace v7.7.2).
- app-creator: 1 SKIP -> 16 judged findings (3 ERROR, 2 WARN). Recorded, not
  fixed — it records, it does not fix.
- Nothing else moved.
- `--refresh`: 22/22, no divergence.

The accepted lists are claims about three OTHER repos, measured from LOCAL
clones — which is the gap REMOTE-SYNC exists to name. They go to those repos by
coord so a wrong name can be disputed.

187 tests (was 182).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AkHEqTSr1k3HbeiHu1ggW
2026-08-12 22:24:03 +02:00
fa0cfe07d5 feat(engine): LINK-FILE-URL — a link into someone's home directory
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
2026-08-12 21:27:29 +02:00
ddfc628761 feat(engine): TAG-ANNOTATED — a movable tag is a movable pin
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
2026-08-12 21:24:09 +02:00
3955b10c4d fix(engine): a claim word inside a longer word is not a claim
`selftest_checks-402` — a count of checks that exist — carried
BADGE-STATIC-CLAIM through censuses 03, 05 and 06 because `tests?`
matched the letters inside "selfTESTs". repo-mailbox disputed it every
round; org-ops measured and concluded the finding was false (coord,
2026-08-12).

Their proposed test was renaming the visible label to "Checks". Measured
here first: that does NOT clear it, because the URL slug is scanned too.
The rule was reading claim words as substrings anywhere in either.

Matching word by word fixes it. Splitting on every non-alphanumeric run
rather than leaning on `\b` is what avoids the opposite defect —
shields.io writes a space as `_`, so `\btests\b` would have gone quiet on
the genuine claim `tests-402_passing`. Both directions are pinned.

Measured across all 19 local clones: 20 badge findings before, 20 after,
exactly one converted (repo-mailbox WARN -> OK). No other repo moved.

The count-vs-result split org-ops proposed is deliberately NOT built: the
one measured case is fully explained by the substring bug, and a rule for
`tests-402` that no repo has produced would be speculation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lb7XmJGLnFSX9U7tgS7fKk
2026-08-12 21:15:26 +02:00
9073d3535d docs: publish the canonical GOVERNANCE.md the org links to (D11)
The file every repo's README item 16 points at now exists at its decided
home. Content is the org's byte-identical baseline (md5 736fc9d6, 131
lines) deliberately generalised from marketplace/plugin wording to
organisation-wide wording — the substance (solo-maintained, fork-and-own,
no PRs, issues as signals, no SLA, MIT default) is unchanged. The README's
pre-D11 link to the marketplace copy now points here instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W9wLjm8WoZr8RuseYjCFhP
2026-08-12 20:40:29 +02:00
36583646ff test: scope the rename guard to the emission site, not any occurrence
The 0.9.0 guard asserted `!src.includes('VERSION-UNAVAILABLE')`. That is
stricter than the intent: it fails the suite the moment someone writes the
historical name into a comment explaining why the code was renamed — the
archaeology this repo deliberately keeps next to the code. Scoped to
`code: 'VERSION-UNAVAILABLE'` instead.

Mutation-tested: re-introducing the old code name at the emission site fails
2 tests. No behaviour change, so no version bump — v0.9.0 still names this
engine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTNGvxzCLZp3UXfibYG4kb
2026-08-09 22:09:03 +02:00
1dfd408ffc feat(engine)!: "no version to compare against" is a verdict, not a skip
`VERSION-UNAVAILABLE` (SKIP/notRun) becomes `VERSION-NONE` (OK). A repo that
claims no version in any of the four places one can be written down has not
left a check un-run — the check ran, saw all four, and found no subject. That
is the shape `checkReadmeLanguage` has always answered with OK.

0.8.0 deferred this and recorded the reason in the engine: re-levelling "moves
a repo's status". Measured false across 19 clones — an added OK cannot worsen
the worst *judged* finding, and all three repos emitting the code already read
OK. Status moves only for a repo whose entire finding set was skips.

OK cannot bless a real gap here: no class requires a version file, and a
`plugin` missing its manifest is an independent FILE-MISSING ERROR.

Renamed, not just re-levelled: `-UNAVAILABLE` is this engine's naming for a
notRun skip, and one OK-level `-UNAVAILABLE` would mislead exactly the reader
this change serves. A source scan keeps the old name out.

A/B sweep, both engines offline, 19 clones: 223 judged findings, every status
and buckets identical; 3 conversions (notChecked 14→13, 3→2, 14→13), byDesign
untouched. 170 tests. `--refresh`: register 21 = forge 21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTNGvxzCLZp3UXfibYG4kb
2026-08-09 22:05:21 +02:00
9a566240e8 docs: the skill stopped contradicting itself about what a SKIP is
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
2026-08-09 21:55:58 +02:00
f4017bcfe1 feat(engine)!: a skip says which of the two things it is
`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
2026-08-09 21:52:00 +02:00
1a647a591f docs: the skill and the README caught up with 0.7.0
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.
2026-08-09 21:34:20 +02:00
10ad1254ab feat(engine)!: SKIP stops outranking OK, coverage gets its own axis
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.
2026-08-09 21:29:08 +02:00
5884a64e54 feat(register): a decided title, and an org-profile that stops lying
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
2026-08-09 21:14:43 +02:00
a7276e6f78 feat(register): register llm-security-commons as shared-asset
The org's 21st repo (register: 20 → 21, forge: 21 — confirmed by --refresh
last session). Class verified structurally, mirroring c5c11f4: absent from
the catalog's marketplace.json, no .claude-plugin/, README.md + LICENSE
present, `## Non-goals` heading present, consumed by `llm-security` (which
already carries the `security` trait) as a git subtree per its own README
Install section. So `vendor` is the right install form.

Trait left unset: llm-security-commons' own STATE.md names this as an open
operator decision, and the register's trait definition keys traits to what
code DOES — this repo states "no runnable code, data/specifications/
fixtures only."

Measured, not assumed:
- llm-security-commons now gates ERROR (10x LINK-INTERNAL-MISSING) — its
  README documents the target directory layout (lexicon/, codepoints/,
  signatures/, calibration/, mapping/, schema/, spec/, conformance/) but
  `git ls-files` shows only signatures/active-content.json and
  docs/extraction-plan.md exist; neither was flagged, so the check found
  real gaps, not noise. Findings recorded, not fixed here — that repo's
  own session owns them.
- llm-security (the plugin that vendors this) still reports zero ERROR:
  no `open/llm-security-commons` reference exists anywhere in the org
  outside the commons repo itself, so no dependent verdict moved. Matches
  what STATE predicted before this session started.

Not tagged as a release: catalog's ref is still on v0.3.0, two releases
(v0.4.0, v0.5.0) behind and already asked for via coord (unactioned,
latest ask reply-expected: no). Stacking a third release on top would only
widen that gap; this commit reaches installed consumers on whichever
release picks it up next, same as portfolio-optimiser-commons did in
c5c11f4 -> 43a8d28.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjNqnudJnH4Q9ZyVWh8UDr
2026-08-09 14:45:52 +02:00
63c75d841c chore(release): v0.5.0, since LINK-INTERNAL-FIXTURE is a new check 2026-08-09 14:31:55 +02:00
9eb210bb01 feat(engine): fixture-path dead links are SKIP, not WARN
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).
2026-08-09 14:31:18 +02:00
43a8d28f00 chore(release): v0.4.0, since LINKS-OPEN-REFS is a new check
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
2026-08-09 14:18:32 +02:00
c8cbeb97a5 fix(engine): a schemaless host is still a host, and OK is a real outcome
Two findings from org-ops census 03, both measured against the parser
rather than read out of the regex.

(a) URL_REF required :// or @host:, so `git.fromaitochitta.com/open/<name>`
— the form a subtree instruction routinely uses — extracted nothing at
all (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 the noise the scheme was
masking: 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 were references that had
been invisible. The 14th was a defect this widening introduced — a
markdown link whose display text repeats its own URL matched on both
halves, printing one dead reference twice and inflating the count the
new OK line offers as evidence. References are now deduplicated per
name-and-line, so two names on one line, or one name on two lines,
still count as two.

(b) checkLinks emitted nothing on success, so "no dead references" and
"the check never ran" were identical in the output — a sweep could not
tell 19 clean repos from 19 unread ones. The file already applies
"three outcomes, never two" to classifyRef; it now applies it to its
own result. Zero enumerated files is LINKS-OPEN-REFS-UNAVAILABLE
(SKIP), which is the honest name for what used to look like a pass.

Caught by the gate against itself: the first draft of this release's
CHANGELOG entry used a literal open/<name> placeholder and became a
real LINK-DEAD ERROR. Rewritten, not exempted.

116 -> 129 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxP9N3p1fG6UYSB8rATBL6
2026-08-09 14:18:24 +02:00
c5c11f4b13 feat(register): register portfolio-optimiser-commons as shared-asset
The org's 20th repo, missing since it was published on 2026-08-04 and
reported by three independent senders (the repo itself, org-ops census
03d, org-ops census 04).

Class verified structurally rather than accepted on the proposal:
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 requiring `## Install` would have been wrong.

The cost of the gap was not a gap. classifyRef returns `unknown` for
an unregistered name and `unknown` is the ERROR branch, so
portfolio-optimiser earned three false LINK-DEAD ERRORs against a repo
that exists — in the same round it fixed its three real ones. Verified
after: portfolio-optimiser has zero ERROR, and commons measures as
WARN with 9 checks run instead of SKIP with none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxP9N3p1fG6UYSB8rATBL6
2026-08-09 14:18:09 +02:00
f28615f3b6 chore(release): v0.3.0, since BADGE-DEAD-LINK is a new check
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
2026-08-04 22:29:02 +02:00
5cb28503fe docs: correct test count from 113 to 116
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
2026-08-04 22:21:00 +02:00
aefbadf9a8 fix(engine): close the linked-badge blind spot, de-tautologize 2 tests
Two remaining minor findings from STATE.md:

checkBadges treated being wrapped in a markdown link as proof of a
real run and skipped BADGE-STATIC-CLAIM entirely — but nothing ever
checked the link actually resolved. The link-target capture group was
missing from the regex outright. Now a linked run-claim badge with a
relative target is resolved against `present` (new BADGE-DEAD-LINK,
ERROR/broken): a dead link is worse than a static badge because it
looks verified. External targets (the ordinary case — a CI provider)
still need the network and stay out of scope, same precedent as
checkInternalLinks.

The "four pre-existing tautological tests" note undercounted on
re-measurement: only two exist ('no class requires a ROADMAP', 'CONTRIBUTING
and CODE_OF_CONDUCT are required by no class'). Both read
`required_files` off the test file's OWN local REGISTER fixture, so
they could only ever check the fixture against itself — a typo in the
real register/repos.json would drift past them silently. Switched
both to `loadRegister()` and verified the fix is real: temporarily
added ROADMAP.md to a class in the live register and confirmed the
test goes red, then restored it clean.

113 -> 116 tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
2026-08-04 22:20:36 +02:00
e6b0f04021 fix(engine): widen 429 backoff budget, correct the rate-limit explanation
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
2026-08-04 22:09:21 +02:00
568b8e374a fix(engine): LINK-DEAD false positive on API endpoint paths
URL_REF matched `open/<name>` anywhere in a URL path, so a Forgejo API
call like `.../api/v1/orgs/open/repos` read as a dead reference to a
repo named "repos" — "open" there is the org argument to the API, not
a repo reference. Restrict the host segment to exclude `/`, so `open`
must 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:39 and :114.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
2026-08-04 22:01:53 +02:00
ca81db549c docs: correct test count from 103 to 111
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
2026-08-04 21:53:58 +02:00
10 changed files with 3625 additions and 95 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "repo-standard", "name": "repo-standard",
"version": "0.2.2", "version": "0.11.1",
"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.", "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": { "author": {
"name": "Kjell Tore Guttormsen" "name": "Kjell Tore Guttormsen"

View file

@ -4,6 +4,529 @@ All notable changes to this project are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
versioning is [Semantic Versioning](https://semver.org/spec/v2.0.0.html). versioning is [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.11.1] — 2026-08-13
### Fixed
- **`TAGS-SIGNED`, the affirmative verdict, was near-unreachable.** It was gated
on `findings.length === 0`, so the presence of a `TAG-SIGNED-PREPOLICY` `OK`
suppressed it — and every repo in the org has pre-policy tags. Caught by
dogfooding the v0.11.0 release: this repo signed the first tag the check ever
judged, and the gate did not say so.
The status was green either way, which is what made it worth fixing rather
than shrugging at: a reader could not tell *this repo signed its new tag* from
*this repo has cut no tag since the policy*. Two different facts wearing one
silence — the same defect as an exemption nobody can see, which this engine
already refuses to ship one check over. The gate now keys the affirmative on
the JUDGED findings only, and the two cases carry different sentences.
Measured after: 18 repos emit both `TAGS-SIGNED` and `TAG-SIGNED-PREPOLICY`,
3 `TAG-SIGNED-NONE`, still 0 ERROR and 0 WARN. 243 tests, from 241.
## [0.11.0] — 2026-08-13
### Added
- **`TAG-SIGNED` — a release tag that names a signer, not just whoever could
write to the forge.** The catalog pins plugins by tag, so this sits on the
same supply-chain axis as `TAG-ANNOTATED`: annotation makes a tag
immovable-without-a-trace, a signature makes it attributable.
The check had been parked for a release 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 (operator, 2026-08-13) asks something else entirely: *would a public
AAA+ repository do this?* Yes. Unlike the two legitimate conventions that
killed `VERSION-DRIFT`, there is no second convention under which release tags
are better left unattributable.
**The policy is a DATE, and the date is what made the check shippable.**
Measured across 20 clones before the rule was locked: 158 `v*` tags, 144
annotated, 14 lightweight, 0 signed, and not one tag dated after 2026-08-12. A
boolean "this org signs" would have failed 20 correct repositories the day it
landed, with force-moving 144 published refs as its only remedy — the mechanism
that gets gates switched off. `tags_signed_from` lives in the register, never
in the engine (the rule that keeps the org-profile exemption a flag rather than
a class name in a classifier); absent it, the check is a `notRun` SKIP, because
the gate never assumes a policy nobody recorded.
Verified both directions: a sweep of 21 repos yields 18 `TAG-SIGNED-PREPOLICY`
+ 3 `TAG-SIGNED-NONE`, 0 ERROR, 0 WARN — then, against real git objects, an
unsigned post-policy newest tag does fire `ERROR`. A sweep returning zero
proves nothing alone; a dead check returns zero too.
Two exclusions keep one defect from being counted twice under two codes: a
**lightweight** tag has no tag object to carry a signature at all
(`TAG-SIGNED-LIGHTWEIGHT`, a `byDesign` skip naming `TAG-ANNOTATED` as the
owner), and a **pre-policy** tag was correct when it was cut
(`TAG-SIGNED-PREPOLICY`, an `OK` — said out loud, never as silence).
It answers *is this tag signed* and never *does the forge vouch for it*: the
signature is read from the clone, while the forge's "Verified" badge needs a
key registered there.
### Changed
- The engine reads tag signature and creation date alongside the object type, in
the same single `for-each-ref`. The signature is read with
`%(if)%(contents:signature)%(then)…` rather than by printing the block, which
is multi-line and would break the line-oriented parser on exactly the tags the
check cares about. No new API call — this stays at three.
- 241 tests, from 230.
## [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 ## [0.2.2] — 2026-08-04
### Fixed ### Fixed

296
CLAUDE.md
View file

@ -28,7 +28,47 @@ would recreate, in data, exactly the drift this plugin exists to remove.
not belong here. It is measured where the org is enumerated. not belong here. It is measured where the org is enumerated.
- **It records, it does not fix.** Findings first, remediation afterwards. - **It records, it does not fix.** Findings first, remediation afterwards.
Patching while measuring is how the inconsistency it detects was produced. Patching while measuring is how the inconsistency it detects was produced.
- **`SKIP` is never a pass.** A check that could not run says so and names why. - **`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.
- **When a check fires many times in one repo, suspect the CHECK.** The first - **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 — link pass produced ~30 findings against `llm-security` and all were noise —
regexes in code spans, `file:` URLs, relative paths resolved against the wrong regexes in code spans, `file:` URLs, relative paths resolved against the wrong
@ -48,19 +88,115 @@ would recreate, in data, exactly the drift this plugin exists to remove.
registered (measured). The substitute is one command from a clean clone, said 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 — plainly. A static badge asserting a run is the anti-pattern this gate flags —
and an early draft of this README carried one. 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" - **Three outcomes on references.** "No match" and "match on a known non-repo"
must stay distinct findings. Collapsing them hides real loss inside correct must stay distinct findings. Collapsing them hides real loss inside correct
text — the exact defect class this gate exists to catch. text — the exact defect class this gate exists to catch.
- **Two API calls per invocation, anonymous, with 429 retry.** The org listing - **Three API calls per invocation, anonymous, with 429 retry.** The org listing
(description + topics) is one; the catalog's `marketplace.json` for (description + topics) is one; the catalog's `marketplace.json` for
INSTALL-TRUTH is the other (added after this used to say "one call" — that INSTALL-TRUTH is the second (added after this used to say "one call" — that
line went stale and stayed stale until a 13-repo shell loop trusted it and line went stale and stayed stale until a 13-repo shell loop trusted it and
tripped the rate limiter at 26 requests). Both go through `fetchWithRetry`, tripped the rate limiter at 26 requests); the repo's releases listing for
which honors `Retry-After` on HTTP 429 rather than silently reporting SKIP. `RELEASE-CURRENT` is the third, and is the only subject the cheaper git
Both are anonymous — no token — so the gate works for any reader, not only channel cannot cover, because a release is not a ref. This count is the line
someone holding one. A sweep across every repo still does not belong here: that has now gone stale twice — it is updated in the SAME commit as the check
it needs the listing fetched once, not once per invocation, which is a that changes it, never afterwards. All go through `fetchWithRetry`,
different shape of caller (org-ops), not a flag on this engine. 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.
- **Codepoints, not bytes, not UTF-16 units.** Use `[...s].length`. An em-dash - **Codepoints, not bytes, not UTF-16 units.** Use `[...s].length`. An em-dash
exposes only the byte layer; astral characters expose the rest. exposes only the byte layer; astral characters expose the rest.
- **The reader decides a link's level, not just what is required.** Root - **The reader decides a link's level, not just what is required.** Root
@ -68,16 +204,147 @@ 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 root it is a `WARN`: that is where session plans, agent working files and
path-traversal fixtures with deliberately invalid targets live. Measured, 30 path-traversal fixtures with deliberately invalid targets live. Measured, 30
of 43 findings were down there and all were `ERROR`s. 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 - **A repo's name is its remote, not its directory.** `catalog/` holds
`ktg-plugin-marketplace`. The basename left it unregistered with zero checks `ktg-plugin-marketplace`. The basename left it unregistered with zero checks
run, against the one repo every catalog rule depends on. 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.
- **The question is "would a public AAA+ repo do this", NOT "how many of ours
already do".** Operator rule, 2026-08-13, and it settled `TAG-SIGNED` — which
had been parked as "blocked on an operator decision" on the strength of 0 of
18 repos signing. That zero was never evidence against the check: 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 operator's word for being
asked at all was that it "should almost be self-evident". This is genuinely
different from the VERSION-DRIFT rejection, where two legitimate conventions
coexisted; there is no second convention under which release tags are better
left unattributable. **The test decides WHETHER; the corpus measurement then
decides only HOW TO SCOPE** so the rule does not fail correct repositories on
day one. Do not let a low adoption rate veto a yes.
This does not reopen `RELEASE-ASSETS`, which was rejected for the stronger
reason — no subject at all, 0 of 21 READMEs referencing an asset download.
- **`TAG-SIGNED` ships as a policy DATE, and the date is what made it
shippable.** Measured across 20 clones before the rule was locked (2026-08-13):
158 `v*` tags, 144 annotated, 14 lightweight, **0 signed**, and not one tag
dated after 2026-08-12. A boolean "this org signs" would have failed 20
correct repositories the day it landed, with force-moving 144 published refs
as its only remedy — the mechanism that gets gates switched off. The date
(`tags_signed_from` in the register, never in the engine, the same rule that
keeps the org-profile exemption a flag) fires nothing today and acquires teeth
at each repo's next release, which is how signing is adopted in public.
Verified by sweep: 21 repos, 18 `TAG-SIGNED-PREPOLICY` + 3 `TAG-SIGNED-NONE`,
0 ERROR, 0 WARN — then positively controlled against real git objects, where
an unsigned post-policy newest tag does fire `ERROR`. A sweep returning zero
proves nothing on its own; a dead check returns zero too.
Two exclusions keep one defect from being counted twice: a **lightweight** tag
has no tag object to carry a signature (`TAG-ANNOTATED` owns it, and the skip
names that owner), and a **pre-policy** tag was correct when it was cut — said
out loud as an `OK`, never as silence. Scope fixed in advance and held:
the signature is read from the clone, so the check answers *is it signed* and
never *does the forge vouch for it* — that badge needs a key registered there.
Read with `%(if)%(contents:signature)%(then)…` rather than by printing the
block, which is multi-line and would break the line-oriented parser on exactly
the tags that matter.
- **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 - **No hook until the rule is precise.** A blocking gate that fails a correct
repository is the mechanism that gets gates switched off. repository is the mechanism that gets gates switched off.
## Commands ## Commands
```bash ```bash
npm test # 103 tests npm test # 243 tests
node scripts/repo-standard-check.mjs --dir "$PWD" # gate one repo 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 --offline # no network call
node scripts/repo-standard-check.mjs --json # machine output node scripts/repo-standard-check.mjs --json # machine output
@ -86,6 +353,15 @@ node scripts/repo-standard-check.mjs --refresh # register vs. forge
## Release ## 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 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 **and** the catalog `ref` is bumped to it. Use `release-plugin.mjs`, never a
hand-edited `ref`. hand-edited `ref`.

131
GOVERNANCE.md Normal file
View file

@ -0,0 +1,131 @@
# 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.

152
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 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. checks that surface in one repository and reports what it finds.
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See the [marketplace governance](https://git.fromaitochitta.com/open/ktg-plugin-marketplace/src/branch/main/GOVERNANCE.md) for the full model. > **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.
*AI-generated: all code produced by Claude Code through dialog-driven development.* *AI-generated: all code produced by Claude Code through dialog-driven development.*
![Version](https://img.shields.io/badge/version-0.2.2-blue) ![Version](https://img.shields.io/badge/version-0.11.1-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple) ![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Skills](https://img.shields.io/badge/skills-1-orange) ![Skills](https://img.shields.io/badge/skills-1-orange)
![License](https://img.shields.io/badge/license-MIT-lightgrey) ![License](https://img.shields.io/badge/license-MIT-lightgrey)
@ -41,10 +41,17 @@ a replacement for the two commands above:
## Requirements ## Requirements
Node 18 or newer. No dependencies. Two network calls: the org listing (for a Node 18 or newer. No dependencies. Three API calls: the org listing (for a
repo's published description) and the catalog manifest (to confirm the install repo's published description), the catalog manifest (to confirm the install
command resolves). Both read anonymously, so no token is needed, and `--offline` command resolves), and the repo's releases listing (to see the version page a
skips both — the checks that depended on them then report `SKIP`, not `OK`. 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.
## What it does ## What it does
@ -61,6 +68,7 @@ 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 | | 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 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 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 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 | | Required files | a file this class (or trait) needs is absent |
| Repo references | an `open/<name>` in URL position resolves to nothing | | Repo references | an `open/<name>` in URL position resolves to nothing |
@ -71,6 +79,11 @@ 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 | | README language | the prose is not in the language this repo's readers were declared to speak |
| Boilerplate | template text nobody filled in | | Boilerplate | template text nobody filled in |
| Version consistency | manifest, README badge, newest CHANGELOG entry and the git tag disagree | | 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 |
| Tag signing | a tag cut on or after the org's signing date carries no signature, so nothing ties the released ref to a signer rather than to whoever could write to the forge. Tags predating the date are never judged — signing them now would mean force-moving published refs. Answers *is it signed*, never *does the forge vouch for it* |
| 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 | | Description | empty, or past the length bound |
Findings carry two independent things: a **level** (`ERROR`, `WARN`, `SKIP`, Findings carry two independent things: a **level** (`ERROR`, `WARN`, `SKIP`,
@ -80,9 +93,51 @@ Findings carry two independent things: a **level** (`ERROR`, `WARN`, `SKIP`,
- **missing** — an expected artefact is absent - **missing** — an expected artefact is absent
- **weakening** — present and working, but it reads as amateur - **weakening** — present and working, but it reads as amateur
The process exits 1 on any `ERROR`. A `SKIP` means the check could not run — an The process exits 1 on any `ERROR`. A `SKIP` is never a pass and never a
unreachable forge, an untagged repo, a link that leaves the repository. It is severity — it is the absence of a verdict, printed apart from the judged
not a pass, and the output prints those separately under a heading that says so. 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.
### Traits — a second axis ### Traits — a second axis
@ -104,10 +159,63 @@ 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 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 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 still be badly written. Where the prose is too evenly bilingual to call, the
finding is a `SKIP` — the question is live and unanswered. Where there is no finding is a `SKIP`, and a `notRun` one — the question is live and unanswered,
and fixing the prose or the register's `locales` entry answers it. Where there is no
running prose at all, it is an `OK`: nothing claims a language, and a thin README 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. 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 ### What is deliberately not required
`CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` and `MAINTAINERS.md` are required by no `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` and `MAINTAINERS.md` are required by no
@ -147,6 +255,28 @@ 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 — 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. 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 ## Non-goals
- **Anything requiring a view across every repository at once.** This gate sees - **Anything requiring a view across every repository at once.** This gate sees
@ -167,7 +297,7 @@ without that, a raw scan turns three dead names into about twenty.
npm test npm test
``` ```
103 tests over the pure classifiers. The reference fixtures are measured false 170 tests over the pure classifiers. The reference fixtures are measured false
positives, each with its expected verdict — the six that produced the positives, each with its expected verdict — the six that produced the
three-outcome reference rule, plus the noise sources found by running the gate 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 against a real repository: regexes inside code spans that are markdown links to

View file

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

View file

@ -30,11 +30,14 @@
"repo-standard": "plugin", "repo-standard": "plugin",
"ktg-plugin-marketplace": "catalog", "ktg-plugin-marketplace": "catalog",
"playground-design-system": "shared-asset", "playground-design-system": "shared-asset",
"portfolio-optimiser-commons": "shared-asset",
"llm-security-commons": "shared-asset",
".profile": "org-profile", ".profile": "org-profile",
"portfolio-optimiser": "standalone", "portfolio-optimiser": "standalone",
"portfolio-optimiser-claude": "standalone", "portfolio-optimiser-claude": "standalone",
"llm-ingestion-pipeline-security": "standalone", "llm-ingestion-pipeline-security": "standalone",
"llm-ingestion-okf": "standalone" "llm-ingestion-okf": "standalone",
"app-creator": "standalone"
}, },
"$comment_non_repos": [ "$comment_non_repos": [
@ -69,7 +72,18 @@
"", "",
"Consumer-facing documentation is unaffected by working alone, and that is", "Consumer-facing documentation is unaffected by working alone, and that is",
"the whole distinction: SECURITY.md, LICENSE, CHANGELOG, non-goals and honest", "the whole distinction: SECURITY.md, LICENSE, CHANGELOG, non-goals and honest",
"limitations exist for the reader, not for a contributor." "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."
], ],
"classes": { "classes": {
"plugin": { "plugin": {
@ -90,7 +104,8 @@
"org-profile": { "org-profile": {
"required_files": ["README.md"], "required_files": ["README.md"],
"required_headings": [], "required_headings": [],
"install": "none" "install": "none",
"readme_desc_match": false
}, },
"standalone": { "standalone": {
"required_files": ["README.md", "LICENSE"], "required_files": ["README.md", "LICENSE"],
@ -99,6 +114,36 @@
} }
}, },
"$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": [ "$comment_traits": [
"A SECOND axis, orthogonal to class. Class is structural (read off the", "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", "catalog and the remotes); a trait is about what the code DOES, which no",
@ -141,6 +186,59 @@
"okr": "nb" "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"]
},
"$comment_tags_signed_from": [
"The date the org's tag-signing policy takes effect. Operator decision",
"2026-08-13, on the rule that decides these: would a public AAA+ repository",
"do this? Yes — a signed tag ties a released ref to a signer rather than to",
"whoever could write to the forge, and the catalog pins plugins by tag.",
"",
"It lives HERE and not in the engine for the reason the org-profile",
"exemption is a flag rather than `if (klass === 'org-profile')`: the engine",
"holds no org decision of its own. Remove the key and TAG-SIGNED goes back",
"to a notRun SKIP — the gate never assumes a policy nobody recorded.",
"",
"A DATE, not a boolean, and that is the whole reason the check is shippable.",
"MEASURED across 20 clones 2026-08-13: 158 `v*` tags, 144 annotated, 14",
"lightweight, 0 signed, and NOT ONE tag dated after 2026-08-12. A boolean",
"would have failed 20 correct repositories the day it landed — the mechanism",
"that gets gates switched off — and its only remedy would have been force-",
"moving 144 published refs. The date fires nothing today and acquires teeth",
"at each repo's next release, which is how signing is adopted in public.",
"",
"Do NOT backdate this to 'catch up' history. Every tag below it was correct",
"when it was cut, and the check says so out loud (TAG-SIGNED-PREPOLICY, OK)",
"rather than going silent about it."
],
"tags_signed_from": "2026-08-13",
"description_max_codepoints": 180, "description_max_codepoints": 180,
"$comment_length": [ "$comment_length": [
"180 codepoints, not bytes and not UTF-16 units. The same string measures 248", "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,14 +25,39 @@ the script cannot encode.
node "${CLAUDE_PLUGIN_ROOT}/scripts/repo-standard-check.mjs" --dir "$PWD" node "${CLAUDE_PLUGIN_ROOT}/scripts/repo-standard-check.mjs" --dir "$PWD"
Findings are `ERROR` (blocks), `WARN` (look, then decide), `SKIP` (the check Findings are `ERROR` (blocks), `WARN` (look, then decide), `SKIP` (not judged —
could not run), `OK`. Exit 1 on any ERROR. Add `--offline` to skip the two in one of two ways, see below), `OK`. Exit 1 on any ERROR. Add `--offline` to skip the three
network calls, `--json` for machine output, `--refresh` to compare the bundled API calls — the org listing, the catalog manifest, and the repo's releases —
register against the live org listing. `--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.
**Never report a `SKIP` as a pass.** A SKIP means the gate could not see enough **Never report a `SKIP` as a pass.** A SKIP means the gate did not judge the
to judge — an unreachable forge, an untagged repo, a link leaving the thing. Say which, and say which KIND — since 0.8.0 there are two, and they are
repository. Say which. 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.
**Confirm the version before you trust a green run.** Measured 2026-08-04: **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 `${CLAUDE_PLUGIN_ROOT}` resolved to a cached `0.1.1` in a live session while
@ -134,6 +159,53 @@ 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 with the fact that nothing runs it automatically. Verifiability is the goal; CI
is one mechanism for it, and it is not available here. 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.
## Tag signing — forward only, and never by rewriting history
Since 0.11.0 the gate judges whether a release tag is signed. The org's policy
date lives in the register (`tags_signed_from`); tags cut before it are reported
as `TAG-SIGNED-PREPOLICY` (`OK`) and are **not** findings. If the key is absent
the check is a `notRun` SKIP — the gate never assumes a policy nobody recorded.
`TAG-SIGNED` (`ERROR`) is the newest tag cut under the policy, unsigned. The
remedy is to re-cut that one tag signed. `TAG-SIGNED-HISTORY` (`WARN`) is a
count of older ones, and it is a `WARN` precisely because its only remedy is
force-moving an already published ref — the risk the check exists to name. Do
not clear a WARN by doing the dangerous thing.
Two moves look like fixes and are not:
- **Backdating `tags_signed_from` to "catch up".** Every tag below the date was
correct when it was cut. Backdating manufactures findings whose only remedy is
re-signing published history.
- **Re-signing the whole tag history.** Same act, one step further. The org
adopted signing forward for the same reason public projects do.
`TAG-SIGNED-LIGHTWEIGHT` is a `byDesign` skip, not work. A lightweight tag has
no tag object to hold a signature at all, so signing is not a remedy it has —
`TAG-ANNOTATED` owns that finding, and fixing it there is what puts the tag in
this check's scope.
The check reads the signature from the clone, so a green line means *this tag
carries a signature*. It does **not** mean the forge verified it — the forge's
"Verified" badge needs a key registered there, which is a different subject on a
different surface. Do not upgrade the claim when reporting.
## Diagrams — capability is not justification ## Diagrams — capability is not justification
Mermaid renders in a README on this forge. That was verified on the live Mermaid renders in a README on this forge. That was verified on the live