security -> SECURITY.md read as "missing required file for class `standalone`", sending the operator to a class definition that never listed the requirement. requirementsFor now carries each required file's origin (class vs. trait) and checkRequiredFiles names whichever actually required it. Failing test written first per the Iron Law. No release forced by this alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WuWwACwhun35j52AnTxRKV
770 lines
42 KiB
Markdown
770 lines
42 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to this project are documented here.
|
||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
||
versioning is [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||
|
||
## [Unreleased]
|
||
|
||
### Fixed
|
||
|
||
- **`FILE-MISSING` named the class even when a TRAIT required the file.**
|
||
`security` → `SECURITY.md` read as "missing required file for class
|
||
`standalone`", sending the operator looking for a requirement the class
|
||
never listed. `requirementsFor` now carries the source (`class` vs. `trait`)
|
||
alongside each required file, and the message names whichever actually
|
||
required it. 245 tests, from 243.
|
||
|
||
## [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 03–07 may report `OK` from 08 on with an identical finding
|
||
set. Raw census files already carry `engineVersion` and `engineCommit`, which
|
||
is what distinguishes the two readings.
|
||
|
||
### Added
|
||
|
||
- `notChecked` beside `status`, in `--json` and as a `· N not checked` qualifier
|
||
on the summary line. Fixing the status alone would have traded "clean repos
|
||
look skipped" for "skipped checks look clean", which is the worse direction —
|
||
so coverage gets its own axis rather than being folded into the verdict, the
|
||
same split the engine already makes between a finding's level and its bucket.
|
||
The count is the number of `SKIP` findings, which is what the existing
|
||
`NOT CHECKED — these are not passes` section has always listed. An **absent**
|
||
`notChecked` means an older engine and prints the pre-0.7.0 line; it is not
|
||
zero.
|
||
|
||
## [0.6.0] — 2026-08-09
|
||
|
||
### Added
|
||
|
||
- `titles` in the register: an optional per-repo README title. Set, the H1 is
|
||
compared against it and a match is `OK`; unset, the `WARN` stands exactly as
|
||
before. A human title was already a valid choice — `docs/rds-v1.md` prescribes
|
||
`1. # Title` for every class, not `# repo-name` — but a decided YES had
|
||
nowhere to live, so the same 6 `README-H1` WARNs were reported in census 05,
|
||
06 and 07 and would have been reported forever. "We decided this is correct"
|
||
and "nobody has looked at it" now produce different outcomes. Five titles
|
||
registered, each H1 read from the repo rather than copied from the census;
|
||
`ai-psychosis` is deliberately left unregistered, so the one repo where a
|
||
reader cannot connect title to name stands alone. Measured across all 21
|
||
local clones: 6 WARN before, 1 after, no other finding moved.
|
||
|
||
- `engineCommit` alongside `engineVersion`, in the header line (short) and the
|
||
`--json` output (full). The version names a FILE; only the sha names the CODE.
|
||
A sweep stamped 18 raw files `0.4.0` while four carried findings from a check
|
||
that only exists in 0.5.0 — the feature and the version bump are two commits,
|
||
so the worktree held new code under an old number for a window, and the stamp
|
||
lied without being broken. Derived from this checkout, no network call. The
|
||
field is present-and-`null` when underivable, never absent: an absent key
|
||
means an older engine, an explicit `null` means this engine ran and had no
|
||
HEAD to read, and a consumer sorting raw files by stamp needs those to differ.
|
||
|
||
### Changed
|
||
|
||
- `README-DESC` no longer demands byte equality for the `org-profile` class,
|
||
via `readme_desc_match: false` on the class — data in the register, not a
|
||
hardcoded name. For an ordinary repo the README opening and the forge
|
||
description describe the same subject and equality is right; for this one
|
||
class they do not — the README is the organisation's landing page, the forge
|
||
text describes the repo. Both are correct about their own subject, so it is
|
||
the equality that does not apply. A landing page's opening line also carries a
|
||
markdown link while the description field renders as plain text, so passing
|
||
the old check would have meant putting raw markdown on a real surface. The
|
||
class has one member, so coverage is lost nowhere else, and the exemption is
|
||
still RECORDED as an `OK` finding naming the reason rather than dropped.
|
||
Measured: `.profile` went from `ERROR` to 0 ERROR / 0 WARN, and the same
|
||
README under a `plugin` class is still an `ERROR`.
|
||
|
||
## [0.5.0] — 2026-08-09
|
||
|
||
### Added
|
||
|
||
- `LINK-INTERNAL-FIXTURE`: a dead link inside a `test/`, `tests/`,
|
||
`fixtures/`, or `*golden*` path now reports as `SKIP` instead of `WARN`.
|
||
These paths are presumed to break their own links on purpose —
|
||
`nav-golden-escape/bundle/index.md`'s deliberate `../../../../etc/passwd`
|
||
escape pops the whole base path instead of resolving outside the repo, so
|
||
it read as a genuine dead link against three repos in the org. The finding
|
||
still fires with file and line; only the judgement changes. Measured
|
||
before shipping: 16 `LINK-INTERNAL-*` findings before, 16 after, across
|
||
all 20 local clones — every one converted 1:1, none disappeared.
|
||
|
||
## [0.4.0] — 2026-08-09
|
||
|
||
### Added
|
||
|
||
- `LINKS-OPEN-REFS`: the `open/` reference check now reports its own result
|
||
instead of only its failures. Emitting nothing on success made "no dead
|
||
references" and "the check never ran" identical in the output — a sweep
|
||
across the org could not tell 19 clean repos from 19 unread ones (measured:
|
||
org-ops census 03b). The OK line carries the count it resolved, because an
|
||
OK that cannot say how many it checked is the same silence in a different
|
||
colour. Zero enumerated files is now `LINKS-OPEN-REFS-UNAVAILABLE` (SKIP),
|
||
which is the honest name for the case that used to look like a pass.
|
||
- `portfolio-optimiser-commons` registered as `shared-asset`, the org's 20th
|
||
repo. Class verified structurally rather than accepted: absent from the
|
||
catalog's `marketplace.json`, no `.claude-plugin/`, `README.md` + `LICENSE`
|
||
present, consumed by both reference implementations as a `git subtree` —
|
||
so `vendor` is the right install form and `## Install` would be wrong to
|
||
require. Its absence was producing three false `LINK-DEAD` ERRORs in
|
||
`portfolio-optimiser` against a repo that exists, and leaving repo #20 with
|
||
zero coverage. Verified after the fact: `portfolio-optimiser` now has no
|
||
ERROR, and the commons repo measures as WARN with 9 checks passed.
|
||
|
||
### Fixed
|
||
|
||
- `LINK-DEAD`/`LINK-NON-REPO` false negative on a schemaless host. `URL_REF`
|
||
required `://` or `@host:`, so `git.fromaitochitta.com/open/<name>` — the
|
||
form a subtree instruction routinely uses — resolved to nothing at all
|
||
(measured: llm-security/V3-UPGRADE.md:343). What makes a name resolvable is
|
||
its position after a host, not the scheme in front of it. Two guards keep
|
||
the widening from becoming noise: the host must end in a TLD-shaped label,
|
||
and a candidate preceded by `/` is a path segment that merely contains a
|
||
dot, not a host — so `docs/v1.2/open/` and `test/nav.golden/open/` stay
|
||
silent, and the API-endpoint rule from 0.3.0 is untouched. Measured before
|
||
shipping across 1501 tracked Markdown files in 20 local clones: 14 lines
|
||
changed verdict, 13 of them references that were previously invisible.
|
||
- The 14th was a defect the widening introduced, caught by that same
|
||
measurement and fixed here: a markdown link whose display text repeats its
|
||
own URL matched on both halves, which prints one dead reference twice and
|
||
inflates the count the new OK line offers as evidence. References are now
|
||
deduplicated per name-and-line — two different names on one line, or the
|
||
same name on two lines, still count as two.
|
||
|
||
## [0.3.0] — 2026-08-04
|
||
|
||
### Added
|
||
|
||
- `BADGE-DEAD-LINK`: a linked badge making a run claim ("tests", "build",
|
||
"CI", "passing") is now resolved against the repo's tracked files when the
|
||
link target is relative. Being wrapped in a link previously ended all
|
||
scrutiny — the link-target capture group was missing from the regex
|
||
outright, so a badge linked to a dead relative path passed silently.
|
||
External targets (the ordinary case — a CI provider) still need the
|
||
network and stay out of scope, same precedent as `checkInternalLinks`.
|
||
|
||
### Fixed
|
||
|
||
- `LINK-DEAD` false positive on API endpoint paths: `URL_REF` matched
|
||
`open/<name>` anywhere in a URL, so a Forgejo API call like
|
||
`.../api/v1/orgs/open/repos` read as a dead reference to a repo named
|
||
"repos". `open` must now be the first path segment after the host,
|
||
matching how every real repo URL is shaped. Measured twice against the
|
||
catalog's own `RUNBOOK.md`.
|
||
- 429 backoff was tuned for a hard IP ban that does not exist. Measured
|
||
directly against the live forge: nginx never sends `Retry-After` (the
|
||
branch handling it is dead code in practice), the limit is a leaky bucket
|
||
that can take up to ~15s to drain, and it is IP-based rather than
|
||
token-quota-based — a valid token made no difference. `fetchWithRetry` now
|
||
defaults to a 23s worst-case budget (`retries: 5`, `maxDelayMs: 8000`), up
|
||
from 7s. `CLAUDE.md`'s "13 calls in a loop" explanation corrected to match.
|
||
- Two tests asserted facts about their own local test fixture instead of the
|
||
real `register/repos.json` — a typo in the real register would have
|
||
drifted past them silently. Both now read the live register via
|
||
`loadRegister()`.
|
||
|
||
## [0.2.2] — 2026-08-04
|
||
|
||
### Fixed
|
||
|
||
- The engine never printed its own version, which is how a stale plugin cache
|
||
went unnoticed: two repos independently proved the `/repo-standard` skill
|
||
had resolved `${CLAUDE_PLUGIN_ROOT}` to a cached `0.1.1` while `0.2.0` was
|
||
installed and the catalog pinned it — same instruction, wrong engine, and
|
||
`0.1.1` has neither `BADGE-COUNT` nor `README-LANGUAGE`, so the run looked
|
||
clean instead of incomplete. The header line and `--json` output now both
|
||
carry `repo-standard v<version>`; `SKILL.md` tells the reader to confirm it
|
||
against the catalog pin before trusting a green result. The cache
|
||
resolution itself is the harness's, not this repo's, and is not fixed here.
|
||
|
||
## [0.2.1] — 2026-08-04
|
||
|
||
### Fixed
|
||
|
||
- The gate makes two anonymous forge calls per invocation (org listing +
|
||
catalog `marketplace.json`), not one — this repo's own `CLAUDE.md` said
|
||
"one" from before `INSTALL-TRUTH` added the second, and stayed wrong long
|
||
enough that a 13-repo shell loop trusted the count and tripped an HTTP 429
|
||
at 26 requests. Both calls now go through `fetchWithRetry`, which honors
|
||
`Retry-After` and retries instead of silently reporting `SKIP` on a
|
||
transient rate limit. `CLAUDE.md` corrected to match.
|
||
|
||
## [0.2.0] — 2026-08-04
|
||
|
||
### Added
|
||
|
||
Two presentation checks, both from adversarially-verified research rather than
|
||
taste — `docs/presentation-research-2026-08-03.md` records what the evidence
|
||
supports and, more usefully, what it refuses to support.
|
||
|
||
- `BADGE-COUNT` (`WARN`) — more than five badges. Trockman et al., ICSE 2018
|
||
(n=294,941 npm packages) measured a non-linear relationship with popularity
|
||
inflecting at five, motivated by surveyed maintainers calling over-badged
|
||
READMEs cluttered and "trying too hard". `WARN` and never `ERROR`: the
|
||
coefficient sits in an appendix without CI or p-value, so it carries "more is
|
||
not better" and cannot carry a hard limit. Counting uses a narrower rule than
|
||
the existing claim check — a screenshot or architecture diagram must not be
|
||
counted as clutter. Measured: 8 of 18 `open/` repos are past it.
|
||
- `README-LANGUAGE` (`WARN`) — the prose is not in the language this repo's
|
||
readers were declared to speak, via a new `locales` axis in the register.
|
||
English is the default; `ms-ai-architect` and `okr` are declared `nb` as
|
||
Norway-only in audience. Detection is a stopword-frequency comparison over
|
||
prose with code stripped, so a Norwegian flag name in a shell example cannot
|
||
decide the document. Evenly bilingual prose is a `SKIP` — the question is
|
||
live and unanswered. No running prose is an `OK`: nothing claims a language,
|
||
and a thin README is `checkFirstScreen`'s business. Measured: fires on
|
||
exactly the two declared repos, silent on all sixteen English ones.
|
||
|
||
Deliberately **not** built, because the evidence does not reach: any rule about
|
||
images, diagrams, screenshots or terminal recordings (every such claim was
|
||
refuted 0-3); a README length bound (no evidence-based target exists); a section
|
||
count (would fire on 9 of 18 — textbook "suspect the CHECK"); Mermaid source
|
||
length and `#gh-dark-mode-only` (zero occurrences, and the instance's actual
|
||
limit is not readable via the API, so any threshold would be a guess).
|
||
|
||
### Changed
|
||
|
||
- The skill now carries diagram guidance, deliberately as judgement and not as a
|
||
check. Mermaid is verified to render on this forge; that is capability, not
|
||
justification, and every claim that visuals improve a README was refuted. What
|
||
the guidance covers is a diagram that already exists — height, node labels,
|
||
the silent degradation past `MERMAID_MAX_SOURCE_CHARACTERS`, and the fact that
|
||
`#gh-dark-mode-only` is unavailable below Gitea 1.26.0. The org's own measured
|
||
example renders at 1,459 px and wraps node text mid-word while its neighbour
|
||
sits at 191 px: rendering and rendering well are separate questions.
|
||
|
||
## [0.1.3] — 2026-08-03
|
||
|
||
### Fixed
|
||
|
||
Three more measured false positives, all reported via coord the same day
|
||
0.1.2 shipped, from repos running the gate for the first time.
|
||
|
||
- `extractChangelogTop` truncated a PEP 440 pre-release (`0.5.0a2`) to `0.5.0`,
|
||
so `VERSION-CHANGELOG` disagreed with `VERSION-TAG` — which compares the
|
||
untruncated tag and does not have this problem — over a version the manifest
|
||
and the tag already agreed on. Reported by `llm-ingestion-okf` with a
|
||
repro. An affected repo could reach 0 ERROR only by lying about its version;
|
||
now the pre-release suffix is kept.
|
||
- `BADGE-STATIC-CLAIM` treated a bare `status` badge (`status-alpha`,
|
||
`status-experimental`) as a run claim, the same as `tests-34-passing`.
|
||
A self-declared maturity label asserts no run — the same class as version,
|
||
licence and platform, already exempt. Reported by
|
||
`llm-ingestion-pipeline-security`. `build`/`ci`/`passing` still catch the
|
||
run-asserting compounds ("build status", "CI status"); only the bare word
|
||
is dropped.
|
||
- `BOILERPLATE` flagged a bare `FIXME` even when it named the *convention*
|
||
rather than an instance of it — a scanner's own docs describing what it
|
||
detects ("Flag TODO/FIXME markers…", a table row listing "TODO/FIXME
|
||
comments" as a finding type) read the same as a forgotten marker. Reported
|
||
by `config-audit`, whose product literally is a marker scanner. "TODO/FIXME"
|
||
named together is now read as the convention; a lone `FIXME` is still
|
||
caught.
|
||
|
||
## [0.1.2] — 2026-08-03
|
||
|
||
### Fixed
|
||
|
||
- `checkInternalLinks` compared a link's resolved target only against
|
||
`present` — the set of tracked *files* — so a link to a directory
|
||
(`[x](dir/)`) was reported `LINK-INTERNAL-MISSING` even when every file
|
||
under that directory was tracked. A directory was never a member of that
|
||
set to begin with. Reported by `portfolio-optimiser-claude` (coord
|
||
20260803T194933Z) with a minimal repro; confirmed and traced to the same
|
||
defect in `voyage`, `linkedin-studio` and `portfolio-optimiser` — 12 of the
|
||
71 ERROR findings measured across the org on 2026-08-03 were this one check,
|
||
not twelve separate repo problems. Fixed by deriving the set of directories
|
||
that actually contain a tracked file from `present` itself, and letting a
|
||
link that resolves to one of them pass.
|
||
|
||
## [0.1.1] — 2026-08-03
|
||
|
||
Documentation only. No behaviour change: `scripts/` and `register/` are
|
||
unchanged from 0.1.0 — this release carries version strings and the correction
|
||
below.
|
||
|
||
### Fixed
|
||
|
||
- The 0.1.0 entry said the gate has **eleven** checks. It has twelve, which is
|
||
what both the code and the README table already said. The entry is corrected
|
||
in place, and this release exists so the catalog's pinned tag serves the
|
||
corrected text instead of the wrong count. The count is drift of exactly the
|
||
kind this plugin was built to catch, and it was sitting in the plugin's own
|
||
repository.
|
||
|
||
## [0.1.0] — 2026-07-27
|
||
|
||
First release. Covers the checks that a single repository can answer on its own.
|
||
|
||
### Added
|
||
|
||
- **The gate** (`scripts/repo-standard-check.mjs`) — pure classifiers with all
|
||
I/O resolved into their input, findings tagged `ERROR`/`WARN`/`SKIP`/`OK`,
|
||
exit 1 on `ERROR`. Twelve checks: README first screen, install block, install
|
||
truth, required headings, files required by the repo's class and traits,
|
||
`open/<name>` references, relative links, licence claim, badges, boilerplate,
|
||
version consistency and description length.
|
||
- **Taxonomy register** (`register/repos.json`) — one central file mapping each
|
||
repository to its class, plus the per-class file and install requirements.
|
||
`--refresh` compares it against the live org listing.
|
||
- **Three-outcome reference check** — "matches no repository" (`ERROR`) is a
|
||
separate finding from "matches something that is deliberately not a
|
||
repository" (`WARN`). Sharing an outcome would let real dead links hide among
|
||
correct text. Only names in URL position are treated as references; the `.git`
|
||
suffix is normalised first.
|
||
- **Two-axis findings** — every `ERROR`/`WARN` carries a **bucket** alongside its
|
||
level: `broken` (a stranger is blocked or misled), `missing` (an expected
|
||
artefact is absent), `weakening` (present, but it reads as amateur). Output is
|
||
grouped by bucket, because that is the order findings get acted on.
|
||
- **Traits, a second axis beside class** — class is structural and readable off
|
||
the catalog; a trait is about what the code does. `security` attaches a
|
||
`SECURITY.md` requirement and a `## Known limitations` section.
|
||
- **Required README headings per class** — `## Install`, `## Non-goals`,
|
||
`## Changelog`. A heading present at the wrong depth is its own finding, not
|
||
"missing".
|
||
- **In-repo version consistency** — manifest, README version badge, newest
|
||
CHANGELOG entry and the git tag must agree. An untagged repo `SKIP`s the tag
|
||
comparison rather than failing it.
|
||
- **Badge honesty** — a static badge asserting a test, build or coverage run is
|
||
a claim dressed as evidence. Version, licence and platform badges assert no
|
||
run and are fine.
|
||
- **Install truth, not just install syntax** — the marketplace URL in the install
|
||
block must be the real one (offline, against the register), and the plugin must
|
||
actually be pinned in the catalog (one call; `SKIP` if unreachable). A perfectly
|
||
formed `claude plugin install x@mkt` fails silently when `x` was never pinned,
|
||
and that is the single most disqualifying thing a repo can publish.
|
||
- **Boilerplate, licence-claim and relative-link checks.**
|
||
- **The skill** (`skills/repo-standard/`) — the judgement the script cannot
|
||
encode: what a description should say, why the summary card must never be used
|
||
to verify one, what not to retrofit, and where the per-repo boundary is.
|
||
- **Link level follows the reader.** A dead relative link in a root document —
|
||
README, CHANGELOG, SECURITY — is an `ERROR`: it is in the shop window. The same
|
||
link below the root is a `WARN`. Measured across seventeen repositories: 30 of
|
||
43 findings sat in `shared/`, `docs/plan/` and `.claude/` — session plans,
|
||
agent working files, and path-traversal test fixtures whose targets are invalid
|
||
on purpose. All 30 were `ERROR`s, which is how a gate gets switched off. Only
|
||
the level moves; every finding is still reported with its file and line.
|
||
- **The repo name comes from the remote, not the directory.** `catalog/` is the
|
||
working directory of the repo named `ktg-plugin-marketplace`. Reading the
|
||
basename left it `REPO-UNREGISTERED` with zero checks run — against the one
|
||
repo every catalog rule depends on. The scp form the forge's clone button hands
|
||
out is handled, and a bare host is not a repo name.
|
||
- 84 tests over the pure classifiers, using measured false positives as
|
||
reference fixtures.
|
||
|
||
### Notes
|
||
|
||
- Descriptions are measured in **codepoints** — not bytes, and not UTF-16 units.
|
||
The same string measures 248 / 249 / 253 across those three yardsticks when it
|
||
contains an astral character.
|
||
- `enabledPlugins` in `settings.json` is treated as a legitimate second install
|
||
form. The gate requires a CLI command *as well*, and never reports the JSON
|
||
form as the defect.
|
||
- Two network calls, both anonymous: the org listing and the catalog manifest.
|
||
Per-repo fetching trips the forge's rate limiter, so neither is ever done per
|
||
repository. `--offline` skips both, and the checks that needed them report
|
||
`SKIP` rather than passing.
|
||
- `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` and `MAINTAINERS.md` are required by
|
||
no class: the project is solo-maintained and publishes that stance. This is
|
||
not a rule against having them — files already present are a cleanup decision,
|
||
not a gate finding. `SECURITY.md` is unaffected; it exists for the outsider who
|
||
finds a hole.
|
||
- There is no CI badge because there is no CI: the forge has no Actions runner
|
||
registered. The substitute is one command from a clean clone, stated honestly.
|
||
A badge asserting the test count would be the very anti-pattern the gate flags,
|
||
and an earlier draft of this README carried exactly that.
|
||
- The link and boilerplate checks ignore fenced blocks and inline code spans.
|
||
Documentation about regexes and placeholders is full of text that is a markdown
|
||
link to a naive scanner — the first version produced about thirty findings
|
||
against a real repository and every one was noise.
|
||
- Relative links resolve against the file they appear in, not the repo root.
|
||
- Indented text counts as a code block only when a blank line opens it, and then
|
||
continues while the indent holds. The first rule alone made links inside nested
|
||
list items invisible; the second alone let everything after a block's first line
|
||
leak back in. Both were silent false passes, which is worse than noise — the
|
||
second was caught by the gate on this plugin's own SKILL.md.
|
||
- A README H1 that differs from the repo name is a `WARN`, not an `ERROR`. The
|
||
thread that has to hold is description == catalog == opening line, and the H1
|
||
is none of those three; a human title is a naming choice the operator owns.
|
||
A missing H1 is still an `ERROR`.
|
||
- No hook ships in this release. A blocking gate has to be precise enough not to
|
||
fail a correct repository first.
|
||
|
||
[0.2.0]: https://git.fromaitochitta.com/open/repo-standard/src/tag/v0.2.0
|
||
[0.1.3]: https://git.fromaitochitta.com/open/repo-standard/src/tag/v0.1.3
|
||
[0.1.2]: https://git.fromaitochitta.com/open/repo-standard/src/tag/v0.1.2
|
||
[0.1.1]: https://git.fromaitochitta.com/open/repo-standard/src/tag/v0.1.1
|
||
[0.1.0]: https://git.fromaitochitta.com/open/repo-standard/src/tag/v0.1.0
|