Compare commits

...

27 commits

Author SHA1 Message Date
45a2bf30de feat(engine): per-repo heading alias closes the Non-goals/nb-repo gap
The Non-goals contract is one literal English heading, matched
case-insensitively but never translated — a repo whose readers were
declared `nb` in `locales` could only go green by planting an English
heading inside an otherwise-Norwegian document. Measured on
ki-produktivitetsmodell (order, census 09): `## Virkeområde og
forbehold` already does the job Non-goals exists for, HEADING-MISSING
fired anyway. `heading_aliases` in the register is the same shape
`titles` already is for the README H1 — decision in the repo,
bookkeeping here — keyed per repo so two nb-repos need not share a
Norwegian phrasing. Satisfying a requirement through it is its own OK
(HEADING-ALIAS), never folded silently into the aggregate.

Also books two operator title decisions verified against the live
clones (not the coord messages that reported them): .profile's H1
became "From AI to Chitta — open" (org landing page rebuild, commit
9898a6e), and ki-produktivitetsmodell's H1 "Tre nivå av organisatorisk
produktivitet med KI" is deliberate, not drift.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gh6T6iHHkMvgeBbGaLmLQD
2026-08-20 23:14:05 +02:00
e061c1d219 docs(register): ki-produktivitetsmodell is nb — operator decision
README is written entirely in Norwegian, explicitly for "toppledere i norsk
offentlig sektor" — the same Norway-only-audience test that set okr's locale,
not a code trait. Clears the README-LANGUAGE WARN against the default `en`;
verified against the clone (13 passed vs. 12 before, WARN gone, the other
three findings there are that repo's own to fix — HEADING-MISSING,
README-H1, two real LINK-FILE-URL).

Operator approved on the session's recommendation; titles left open per the
same recommendation (lower priority, no specific title to record yet).
2026-08-18 17:44:30 +02:00
1ee6cc28c6 test(engine): pin inspectRepo's multi-file loading through the real I/O path
Closes a real gap the order (20260818T132719Z) correctly identified even
though its diagnosis was wrong: every LINK-FILE-URL test fed `files` to
checkInternalLinks by hand, so nothing pinned that inspectRepo's own loading
loop (`tracked.filter(p => p.endsWith('.md'))`) actually reaches files other
than README.md. A future narrowing of that filter back to README-only would
have kept all 247 tests green.

Two new tests go through inspectRepo itself (git init + git add in a temp
dir, no commit — avoids the gitleaks pre-commit hook and unnecessary git
config). Verified red-then-green: temporarily replaced the loading loop with
an empty one, confirmed the leak-detection test failed with the exact
"did not fire" message, reverted, confirmed all 249 pass.

This crosses the file's stated "pure classifiers only, inspectRepo is
exercised live" boundary deliberately and narrowly, for the one behaviour an
external report specifically asked to see pinned through the ordinary
loading path rather than by feeding `files` directly.
2026-08-18 17:11:58 +02:00
5fe1743105 docs(engine): record the file:// root-widening decision — order 20260818T132719Z
Answers the order's two asks, verified independently:

1. "sjekken leser bare README.md" — FALSE against the current codebase.
   inspectRepo has scanned every tracked .md file since 2026-07-27 (commit
   816ba97), three weeks before this order. Reproduced the order's exact
   scenario against a fresh clone of open/app-creator: CLAUDE.md:136 does not
   fire, but not from the diagnosed cause — the cited link uses the `/.../`
   elision this engine already treats as a documented placeholder convention
   (the same "18/22 split" the FILE_URL_LEAK comment already describes), not
   a leak. Injected a real (non-elided) file:// leak into the same CLAUDE.md
   in the same clone: LINK-FILE-URL fired immediately, correct file:line.
   No code change corresponds to this half of the order.

2. "bør regelen gjelde file://-lenker generelt, ikke bare /Users|home" — live,
   unmeasured, answered here. Grepped every tracked .md file in the 21 of 22
   registered repos with a local clone (llm-security-commons absent).
   Positive control: the two real /Users/ leaks in ki-produktivitetsmodell's
   CLAUDE.md were found, proving the query isn't silently empty. Result:
   zero occurrences of any other root (no C:\, /private/, /var/, /tmp/) in
   150 file:// hits across 12 repos — every other hit is a generic
   placeholder already declined by the Users|home anchor. Recorded as a
   RELEASE-ASSETS-shaped rejection in the code comment: no subject, not a
   low rate. Widen the day a real one appears.

Closing the order with --no-commit: this resolves both halves, it does not
hand anything back for the sender to act on.
2026-08-18 17:02:27 +02:00
aee0f28539 fix(engine): TAG-ANNOTATED never consulted the register for the newest tag
Reported by catalog (coord, 2026-08-17): register/repos.json has listed
ktg-plugin-marketplace v7.7.2 under tags_lightweight_accepted since the
0.11.2 release (2026-08-14), but `accepted` was only ever applied to
tags.slice(0, -1) — the newest tag is excluded from that slice by
construction, so the entry was dead weight from the day it was written.

checkTagIntegrity now checks the newest tag against `accepted` too, emitting
a distinct TAG-ANNOTATED-ACCEPTED-NEWEST OK instead of silently doing
nothing when the register names it exactly. Acceptance is per-tag-name, not
a standing exemption: a new lightweight tag that becomes newest afterwards
is still judged (tested).

TDD: two new tests written failing first (an existing test's title claimed
"cannot be accepted away" but never actually passed a matching `name`, so it
was accidentally still green either way — reworded to test what it actually
covers). Verified against a fresh clone of ktg-plugin-marketplace: the full
finding set now reads OK TAG-ANNOTATED-ACCEPTED-NEWEST + OK
TAG-ANNOTATED-ACCEPTED(7 older) instead of ERROR, with no knock-on effect on
TAG-SIGNED (all 9 tags predate the signing policy). Register comment updated
to record the three-day dead-weight window rather than silently correcting
it — a decision that turns out wrong is worse than no record.

247 tests pass (245 + 2 new).
2026-08-18 16:59:00 +02:00
4714f13959 docs(register): add ki-produktivitetsmodell as shared-asset
Reported unregistered by ki-leadership (REPO-UNREGISTERED SKIP). Verified
directly against the forge clone rather than trusting the report: no code,
no build chain, no install form — README + CHANGELOG + LICENSE (CC BY 4.0) +
a standalone HTML rendering + a PDF. Matches the other shared-asset entries
(playground-design-system, portfolio-optimiser-commons, llm-security-commons)
far better than standalone, which would demand a false `## Install` heading.

--refresh confirms this was the only drift: 22 registered, 23 on the forge,
exactly this one name missing.

First finding set against the clone: HEADING-MISSING (no `## Non-goals`),
README-H1 and README-LANGUAGE WARNs (title/locale — both operator calls per
this register's own convention, left unset here), and two real LINK-FILE-URL
ERRORs in CLAUDE.md that are not placeholders. That repo's own findings to
fix, not ours.
2026-08-18 16:54:05 +02:00
1a72f8dde5 fix(version): package.json drifted from the v0.11.2 tag it never got bumped to
Confirmed independently by ki-leadership's own gate run (cached engine, but
same root cause). v0.11.2 was tagged and released (f08fcc9) without bumping
package.json — no new tag or catalog ref changes here, just syncing the
version string to what was already published.
2026-08-18 16:53:57 +02:00
1b0a9a338e docs: add SECURITY.md
AAA+ program round 3 (C-axis, trust). No file existed before; new
vulnerability-reporting policy at repo root, matching the org-wide
10/10 template from the coordinated round.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF8cJYs2vZazMisAd95nfM
2026-08-16 21:14:18 +02:00
198c95e5e5 docs: add Table of Contents to README
AAA+ B-axis round 2 (ORDRE 32). 311-line README with 6 H2 sections had
no navigation aid, matching the org-wide pattern (config-audit/README.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187bUAyMzytxGyXcDQJkQKE
2026-08-16 16:18:12 +02:00
f08fcc9b80 chore(release): v0.11.2 — two register decisions and the stale prose counts
Register: ms-ai-architect removed from locales (org-ops D35 — the entry,
not the prose, was the wrong side); ktg-plugin-marketplace v7.7.2 accepted
as lightweight history (catalog's measured counter: monorepo-era tag, zero
consumers). README: the two stale prose counts made count-free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzGk2pnq1N7J9H7xj2XAH1
2026-08-14 20:19:04 +02:00
e5cd1bfaaf fix(engine): FILE-MISSING named the class for a trait-sourced requirement
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
2026-08-13 18:51:21 +02:00
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
11 changed files with 2542 additions and 49 deletions

View file

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

View file

@ -4,6 +4,255 @@ All notable changes to this project are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
versioning is [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.11.2] — 2026-08-14
### Changed
- **Register: `ms-ai-architect` removed from `locales`.** Four sweeps in a row
(0407) held a correct `README-LANGUAGE` `WARN` against an entry that was
itself wrong: the repo's published README disavows a Norway-only audience in
its own words ("useful for any enterprise context", line 17, verified against
the forge), and its 24 Norway markers are traits — what the code does — not
audience. The register was the wrong side; the prose stands. Decided on
org-ops' measured case (their D35).
- **Register: `ktg-plugin-marketplace v7.7.2` accepted as lightweight
history.** Previously deliberately absent because the newest tag has the one
safe remedy. The catalog's measured counter: v7.7.2 is a monorepo-era
llm-security tag (2026-05-19, pre-split), not a catalog release, and zero
install paths consume it — force-moving a published ref for no consumer costs
more than the finding. The catalog committed to annotating every future tag,
which the check itself enforces.
- **README: the two prose counts that had gone stale are now count-free.**
"170 tests" (actual: 245) and "twelve checks" survived two releases
unchanged — the ungated copy of a number is the defect, so the copies are
removed rather than corrected. `npm test` prints the test count; the check
table (20 rows) is the canonical check list.
### 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

207
CLAUDE.md
View file

@ -88,20 +88,38 @@ would recreate, in data, exactly the drift this plugin exists to remove.
registered (measured). The substitute is one command from a clean clone, said
plainly. A static badge asserting a run is the anti-pattern this gate flags —
and an early draft of this README carried one.
- **A dead pin is not a dead link, and it is not a wrong manifest.**
`PIN-DEAD` asks whether the ref a README install command pins actually
resolves — `LINK-DEAD` asks whether the repo exists, `VERSION-TAG` asks
whether the MANIFEST's version was tagged. All three coincided on guard only
because one wrong number was written in three places. It is an `ERROR`
because a dead documentation link costs a stranger a 404 while a dead pin
costs them the install. Resolved against the FORGE, never the clone: a local
tag can exist unpushed, which portfolio-optimiser demonstrates. A pin at a
branch or a sha is a `byDesign` skip — `ls-remote --tags` cannot answer it,
and looseness is a different finding from deadness.
- **Three outcomes on references.** "No match" and "match on a known non-repo"
must stay distinct findings. Collapsing them hides real loss inside correct
text — the exact defect class this gate exists to catch.
- **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
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
tripped the rate limiter at 26 requests). Both go through `fetchWithRetry`,
which retries HTTP 429 rather than silently reporting SKIP. Both are
tripped the rate limiter at 26 requests); the repo's releases listing for
`RELEASE-CURRENT` is the third, and is the only subject the cheaper git
channel cannot cover, because a release is not a ref. This count is the line
that has now gone stale twice — it is updated in the SAME commit as the check
that changes it, never afterwards. All go through `fetchWithRetry`,
which retries HTTP 429 rather than silently reporting SKIP. All are
anonymous — no token, confirmed no different with one — so the gate works
for any reader, not only someone holding one. A sweep across every repo
still does not belong here: it needs the listing fetched once, not once per
invocation, which is a different shape of caller (org-ops), not a flag on
this engine.
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
@ -113,6 +131,72 @@ would recreate, in data, exactly the drift this plugin exists to remove.
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
exposes only the byte layer; astral characters expose the rest.
- **The reader decides a link's level, not just what is required.** Root
@ -141,6 +225,21 @@ would recreate, in data, exactly the drift this plugin exists to remove.
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.
- **A fixed-language contract has a blind spot the `titles` pattern already
knows how to close.** `## Non-goals` is one literal English string, matched
case-insensitively but not translated, so it cannot be satisfied by a repo
whose readers were declared `nb` in `locales` — the only way to go green was
planting an English heading inside an otherwise-Norwegian document. Measured
on `ki-produktivitetsmodell` (order, 2026-08-18, census 09 on that repo):
`nb` locale, `## Virkeområde og forbehold` already doing the job Non-goals
exists for (a stranger sees what the repo does not try to be), `HEADING-
MISSING` `ERROR` regardless. `heading_aliases` in the register is the same
shape `titles` already is for the H1 — the decision is taken in the repo, the
bookkeeping happens here — and satisfying a requirement through it is its own
`OK` (`HEADING-ALIAS`), never folded silently into the aggregate. Keyed PER
REPO, not per locale: a locale-wide translation table would force every
nb-repo into the same Norwegian phrasing for the same section, exactly the
constraint `titles` already rejects for H1s one requirement over.
- **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
@ -158,13 +257,109 @@ would recreate, in data, exactly the drift this plugin exists to remove.
`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
repository is the mechanism that gets gates switched off.
## Commands
```bash
npm test # 170 tests
npm test # 245 tests
node scripts/repo-standard-check.mjs --dir "$PWD" # gate one repo
node scripts/repo-standard-check.mjs --offline # no network call
node scripts/repo-standard-check.mjs --json # machine output

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.

View file

@ -6,11 +6,11 @@ almost never the code — it is the first screen, an install path that stops
halfway, and references to a name that was retired two renames ago. This plugin
checks that surface in one repository and reports what it finds.
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See 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.*
![Version](https://img.shields.io/badge/version-0.9.0-blue)
![Version](https://img.shields.io/badge/version-0.11.2-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Skills](https://img.shields.io/badge/skills-1-orange)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
@ -21,6 +21,15 @@ has no Actions runner — so the test claim is one you run yourself, in one
command, from a clean clone: `npm test`. A badge asserting it would be a claim
dressed as evidence.
## Table of Contents
- [Install](#install)
- [Requirements](#requirements)
- [What it does](#what-it-does)
- [Non-goals](#non-goals)
- [Tests](#tests)
- [Changelog](#changelog)
## Install
Use the `https://` form. The forge UI's clone button hands out an `ssh://` URL,
@ -41,12 +50,17 @@ a replacement for the two commands above:
## Requirements
Node 18 or newer. No dependencies. Two network calls: the org listing (for a
repo's published description) and the catalog manifest (to confirm the install
command resolves). Both read anonymously, so no token is needed, and `--offline`
skips both — the checks that depended on them then report `SKIP`, not `OK`.
Those land as `notRun` and clear on a networked re-run, so an offline pass is
never a fully judged one.
Node 18 or newer. No dependencies. Three API calls: the org listing (for a
repo's published description), the catalog manifest (to confirm the install
command resolves), and the repo's releases listing (to see the version page a
stranger reads). Refs come over the git protocol instead — `git ls-remote
--tags`, which does not spend the API budget — and a release is not a ref, which
is why it is the one subject that needs a call of its own.
All of it reads anonymously, so no token is needed, and `--offline` skips every
one — the checks that depended on them then report `SKIP`, not `OK`. Those land
as `notRun` and clear on a networked re-run, so an offline pass is never a fully
judged one.
## What it does
@ -63,7 +77,8 @@ The repository's **class** decides what each check means:
| First screen | no H1 on line 1, or the line under it is not the published description. An H1 that merely differs from the repo name is a `WARN` — that is a naming choice, not a defect |
| Install block | the form for this class is missing, incomplete, shown over `ssh://`, or points at the wrong marketplace |
| Install truth | the plugin is not pinned in the catalog, so the documented command cannot succeed for anyone |
| Required headings | `## Install`, `## Non-goals`, `## Changelog` — per class. Present at the wrong depth is its own finding |
| 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. A registered per-repo heading alias (`register/repos.json`'s `heading_aliases`) satisfies it too, for a repo whose readers were declared to speak another language |
| Required files | a file this class (or trait) needs is absent |
| Repo references | an `open/<name>` in URL position resolves to nothing |
| Relative links | a link points at a file that is not tracked |
@ -73,6 +88,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 |
| Boilerplate | template text nobody filled in |
| Version consistency | manifest, README badge, newest CHANGELOG entry and the git tag disagree |
| Tag integrity | the newest tag is lightweight — movable without a trace, under a catalog that pins by tag. Older ones are exposed once as a count, because their only "fix" is force-moving a published ref |
| 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 |
Findings carry two independent things: a **level** (`ERROR`, `WARN`, `SKIP`,
@ -87,7 +107,7 @@ severity — it is the absence of a verdict, printed apart from the judged
findings.
A repository's own `status` is the worst of the findings that were *judged*, and
`SKIP` only when nothing was. A skip cannot outrank twelve checks that did run.
`SKIP` only when nothing was. A skip cannot outrank the 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:
@ -114,9 +134,9 @@ 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.9.0 @<sha>)
✗ some-plugin [plugin] — ERROR · 2 not run (repo-standard v0.9.0 @<sha>)
· stranger — SKIP · 1 not run (repo-standard v0.9.0 @<sha>)
✓ 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
@ -169,6 +189,26 @@ 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.
### Registered heading aliases — the same asymmetry, one requirement over
`## Non-goals` (and every other required heading) is one fixed English string,
matched case-insensitively but never translated. A repository whose readers
were declared `nb` in the register's `locales` cannot satisfy it honestly —
the only way to go green was planting an English heading inside an otherwise
Norwegian document, which is worse than the warning it silences.
A `heading_aliases` entry in the register is the fix, shaped exactly like
`titles`: the decision — this section, in this repo's own language, does the
job the English heading names — is made in the repo, and the bookkeeping
happens here. Set one, and a README carrying the aliased heading instead of
the literal one is an `OK` (`HEADING-ALIAS`) that names the alias, never a
silent pass. Leave it out, and `HEADING-MISSING` stands exactly as before.
It is keyed **per repository**, not per locale. A locale-wide translation
table would force every `nb` repository into the same Norwegian phrasing for
the same section — precisely the constraint `titles` already rejects for H1s,
where a human title is a valid choice, not a slot filled from a fixed list.
### When equality is the wrong demand
`README-DESC` requires the README's opening line to be the forge description
@ -186,6 +226,25 @@ A class turns the demand off with `readme_desc_match: false` in the register.
The exemption is still reported as a finding that names the reason: an exception
nobody can see is indistinguishable from a check that silently stopped running.
### One command, because there is no CI
The forge has no Actions runners registered. A green badge here would assert a
run that never happens, which is the dishonest-badge finding one row up. The
substitute this project publishes is one command a stranger can run from a clean
clone, said plainly — so a repository with something runnable and no such command
in its README is a `WARN`.
Two things bound it. It fires only where a subject was **measured**: a
`scripts.test` entry, or a tracked file that is unambiguously an executable
test. Five of the twenty-one clones have neither — they hold prose, output styles
and domain packs — and they answer `VERIFY-NONE` at `OK`, because a check that
ran and found nothing to judge has produced a verdict. Those five span three
different classes, which is why the subject is measured and not read off a class.
And it can only ever fell a **missing** command. Nothing here runs anything, so a
green line means documented, never passing. Measured across the org: six
repositories have a suite their README never mentions.
### What is deliberately not required
`CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` and `MAINTAINERS.md` are required by no
@ -267,7 +326,9 @@ distinct from "an older engine that never tried".
npm test
```
170 tests over the pure classifiers. The reference fixtures are measured false
The suite pins the pure classifiers; `npm test` prints the current count, which
is the only copy of that number this README keeps — a prose count here went
stale twice. The reference fixtures are measured false
positives, each with its expected verdict — the six that produced the
three-outcome reference rule, plus the noise sources found by running the gate
against a real repository: regexes inside code spans that are markdown links to

34
SECURITY.md Normal file
View file

@ -0,0 +1,34 @@
# Security policy
## Reporting a vulnerability
Report privately to <security@fromaitochitta.com> — do not open a
public issue.
Canonical repository: https://git.fromaitochitta.com/open/repo-standard
Please include the affected version or commit, a minimal reproduction,
and the impact you see. We acknowledge every report within 5 working
days, agree a fix and disclosure timeline with the reporter, and aim to
disclose within 90 days of the initial report.
## Response process
1. Acknowledge within 5 working days.
2. Triage and confirm severity within 10 working days.
3. Develop and test a fix.
4. Publish an advisory and credit the reporter unless they prefer
to remain anonymous.
## Supported versions
| Version | Supported |
| ------- | --------- |
| 0.11.x | :white_check_mark: |
| < 0.11 | :x: |
This project has not reached 1.0 yet; only the latest 0.x release line
receives security fixes. See `CHANGELOG.md` for release history.
## Advisories
No advisories have been published yet.

View file

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

View file

@ -32,11 +32,13 @@
"playground-design-system": "shared-asset",
"portfolio-optimiser-commons": "shared-asset",
"llm-security-commons": "shared-asset",
"ki-produktivitetsmodell": "shared-asset",
".profile": "org-profile",
"portfolio-optimiser": "standalone",
"portfolio-optimiser-claude": "standalone",
"llm-ingestion-pipeline-security": "standalone",
"llm-ingestion-okf": "standalone"
"llm-ingestion-okf": "standalone",
"app-creator": "standalone"
},
"$comment_non_repos": [
@ -133,14 +135,27 @@
"",
"`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."
"alone is the wanted effect of registering the others.",
"",
"`.profile` UPDATED 2026-08-18 (org-ops coord, census 09): the org landing",
"page was rebuilt the same day (commit 9898a6e, 'rebuild org landing page,",
"add LICENSE and SECURITY.md', per that repo's own STATE.md — order from",
"`.claude`, AAA+ round 5). The H1 became `# From AI to Chitta — open`; the",
"old value here was the H1 it replaced, not drift. Verified against the",
"clone, not the coord message — the em-dash is U+2014, not a hyphen.",
"",
"`ki-produktivitetsmodell` ADDED 2026-08-18 (order from that repo, on its own",
"operator decision recorded in its STATE.md the same day): `# Tre nivå av",
"organisatorisk produktivitet med KI` is the document's real title, the repo",
"name is a slug. Verified against the clone before writing it down."
],
"titles": {
".profile": "fromaitochitta / open",
".profile": "From AI to Chitta — 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"
"ms-ai-architect": "AI Architect Plugin for Claude Code",
"ki-produktivitetsmodell": "Tre nivå av organisatorisk produktivitet med KI"
},
"$comment_traits": [
@ -174,17 +189,132 @@
"Norwegian readership is `nb`, and is then WRONG in English, not right.",
"",
"PROPOSED BY THE OPERATOR, NOT MEASURED — like `traits`, this list is a",
"judgement and no remote can report it. Both entries below were named by",
"judgement and no remote can report it. Both original entries were named by",
"the operator on 2026-08-04 as Norway-only in their audience.",
"",
"`ms-ai-architect` was REMOVED 2026-08-14 (org-ops coord, their D35): its",
"published README disavows the exclusivity this axis requires — line 17,",
"'while remaining useful for any enterprise context' — verified against the",
"forge before removal. Its 24 Norway markers are TRAITS (what the code",
"does: Digdir, NSM, DPIA), which is the axis confusion this comment warns",
"about two paragraphs up. The register was the wrong side, not the prose.",
"`okr` stands: its README IS Norwegian, rewritten to close census 05.",
"",
"`ki-produktivitetsmodell` ADDED 2026-08-18 (operator decision, on the",
"session's recommendation after registering the repo as `shared-asset`):",
"its README is written entirely in Norwegian, explicitly for 'toppledere i",
"norsk offentlig sektor' — the same Norway-only-audience test that set",
"`okr`, not a code trait.",
"",
"Detection is a stopword-frequency comparison over prose with code stripped.",
"It answers WHICH language dominates, never whether the prose is any good."
],
"locales": {
"ms-ai-architect": "nb",
"okr": "nb"
"okr": "nb",
"ki-produktivitetsmodell": "nb"
},
"$comment_heading_aliases": [
"The Non-goals contract is one fixed English string, matched literally — it",
"cannot be satisfied by a repo whose readers were declared `nb` in `locales`",
"above, and the only way to go green was to plant an English heading inside",
"an otherwise-Norwegian document. Same job `titles` does for a README H1, one",
"requirement over: the decision is taken in the repo, the bookkeeping happens",
"here — a registered alias satisfies the requirement and is its own OK",
"(`HEADING-ALIAS`), never a silent pass.",
"",
"Keyed PER REPO, not per locale: a locale-wide translation table would force",
"every nb-repo into the same Norwegian phrasing for the same section, which",
"is exactly the constraint `titles` already rejects for H1s (a human title is",
"a valid choice, not a slot to fill from a fixed list).",
"",
"ADDED 2026-08-18 (order from ki-produktivitetsmodell, census 09 on that",
"repo): its README is `nb` (see `locales` above) and carries",
"`## Virkeområde og forbehold`, which does the job the Non-goals contract",
"exists for — a stranger sees what the repo does not try to be. The operator",
"decided the README stays as written; this entry is the mechanism, not an",
"exception for one repo. Verified against the clone before writing it down."
],
"heading_aliases": {
"ki-produktivitetsmodell": {
"## Non-goals": "## Virkeområde og forbehold"
}
},
"$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.",
"",
"`ktg-plugin-marketplace v7.7.2` was DELIBERATELY ABSENT until 2026-08-14:",
"as the newest tag it had the one safe remedy (`git tag -a -f`), and",
"pre-accepting it would have let cutting v7.8.0 skip the cheap fix",
"unrecorded. ACCEPTED 2026-08-14 on the catalog's measured counter (their",
"coord, catalog HEAD d0d5d70): v7.7.2 is a monorepo-era llm-security tag",
"(2026-05-19, pre-split), not a catalog release; their README install block",
"pins no ref and all 12 marketplace.json refs are plugin-repo tags, so zero",
"install paths consume it — 'newest tag' here sorts last without being the",
"newest RELEASE, which is the premise the safe-remedy rule rests on.",
"Force-pushing a published ref for zero consumers costs more than the",
"finding. The catalog committed to annotating every future tag; the check",
"itself enforces that, since a new lightweight newest tag is not on this",
"list and fires ERROR.",
"",
"This entry was DEAD WEIGHT from 2026-08-14 to 2026-08-18: `accepted` was",
"only ever consulted for tags OLDER than newest, so v7.7.2 kept firing",
"TAG-ANNOTATED ERROR the whole time — the exact 'we decided this' vs.",
"'nobody looked' collapse this axis exists to prevent, one level down.",
"Caught and reported by the catalog itself (coord, 2026-08-17), fixed in",
"`checkTagIntegrity` (repo-standard, 2026-08-18): the accepted set is now",
"checked against the newest tag too, emitting a distinct",
"`TAG-ANNOTATED-ACCEPTED-NEWEST` OK rather than silently doing nothing.",
"",
"The two slash-named `config-audit/v*` tags on that forge are OUT OF SCOPE",
"by construction, not omitted by a fetch gap: the engine reads",
"`refs/tags/v*` — the repo's own version line — and a namespaced",
"monorepo-era tag is not part of it."
],
"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", "v7.7.2"],
"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,
"$comment_length": [
"180 codepoints, not bytes and not UTF-16 units. The same string measures 248",

View file

@ -432,6 +432,99 @@ export function checkInstallTruth({ name, klass, catalogNames }) {
return [{ level: 'OK', code: 'INSTALL-TRUTH', msg: 'the install command resolves against the catalog' }];
}
// A pin is the one command a stranger actually runs. Reported by org-ops
// (census 08) and re-measured here against the FORGE: 3 pins in the org, 1
// dead — `llm-ingestion-pipeline-security` pins ITSELF to `@v0.7.0`, and that
// tag does not exist (newest is v0.6.1). ERROR, not WARN, because a dead
// documentation link costs a stranger a 404 while a dead pin costs them the
// install.
//
// Deliberately NOT reusing `LINK-DEAD`'s check, only the idea of enumerating:
// LINK-DEAD asks "does the repo exist", this asks "does the reference exist".
// It is also not `VERSION-TAG`, which reads the MANIFEST — the two coincide on
// guard today only because the same wrong number was written in both places.
//
// Resolved against the forge, never the clone: a local tag can exist without
// having been pushed, which is exactly what portfolio-optimiser demonstrates.
const escapeRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export function extractInstallPins(readme, register) {
const forge = String(register?.forge ?? '').replace(/\/+$/, '');
const org = register?.org;
if (!forge || !org) return [];
const re = new RegExp(`(?:git\\+)?${escapeRe(forge)}/${escapeRe(org)}/([A-Za-z0-9._-]+)\\.git@([^\\s"'\`)\\]#]+)`, 'g');
const seen = new Set();
const pins = [];
for (const m of String(readme ?? '').matchAll(re)) {
const key = `${m[1]}@${m[2]}`;
if (seen.has(key)) continue;
seen.add(key);
pins.push({ repo: m[1], ref: m[2] });
}
return pins;
}
// A dotted numeric component is what makes a ref answerable by `ls-remote
// --tags`. `main` and a bare sha are neither dead nor alive to this check.
const TAG_SHAPED = /^v?\d+\.\d+/;
export function checkInstallPins({ readme, forgeTagsByRepo }, register) {
const pins = extractInstallPins(readme, register);
if (pins.length === 0) {
// The VERSION-NONE shape: the check ran, read the whole README, and found
// no subject. Nothing here can be wrong, which is a verdict.
return [{ level: 'OK', code: 'PINS-NONE', msg: 'README pins no install reference — no ref exists here that could be dead' }];
}
const findings = [];
let resolved = 0;
for (const { repo, ref } of pins) {
if (!TAG_SHAPED.test(ref)) {
// A branch or a sha is a different weakness — an unpinned install — and
// this check can never turn it into a verdict, so nobody has an action.
findings.push({
level: 'SKIP',
skip: 'byDesign',
code: 'PIN-NOT-A-TAG',
msg: `install pin \`${repo}@${ref}\` is not a version tag — \`ls-remote --tags\` cannot resolve it, and a branch pin is a looseness this check does not judge`,
});
continue;
}
const tags = forgeTagsByRepo?.[repo];
if (!tags) {
findings.push({
level: 'SKIP',
skip: 'notRun',
code: 'PIN-UNAVAILABLE',
msg: `could not read tags for \`${repo}\` from the forge — the pin \`@${ref}\` was not verified (offline, or the ref listing failed)`,
});
continue;
}
if (!tags.includes(ref)) {
const newest = [...tags].filter((t) => TAG_SHAPED.test(t)).sort(compareTags).slice(-1)[0];
findings.push({
level: 'ERROR',
code: 'PIN-DEAD',
bucket: 'broken',
msg: `README install pin \`${repo}@${ref}\` does not exist on the forge — the one command a stranger runs fails outright${newest ? ` (newest tag is \`${newest}\`)` : ''}`,
});
} else {
resolved += 1;
}
}
// Counts what was actually verified, never what was merely present. The
// first version said "N install pin(s) resolve" whenever no ERROR fired,
// which meant an offline run asserted a pass for a pin nothing had read.
if (resolved > 0) {
findings.push({
level: 'OK',
code: 'PINS',
msg: `${resolved} of ${pins.length} install pin(s) resolve against the forge`,
});
}
return findings;
}
// Requirements come from two axes. The CLASS is structural — it can be read off
// the catalog and the remotes. A TRAIT is about what the code does, which no
// remote can tell you: `security` attaches the obligations a tool acquires by
@ -445,12 +538,19 @@ export function checkInstallTruth({ name, klass, catalogNames }) {
// outsider who finds a hole, and being solo does not remove them.
function requirementsFor(klass, traits, register) {
const cls = register.classes?.[klass] ?? {};
const files = [...(cls.required_files ?? [])];
const classSource = `class \`${klass}\``;
const files = (cls.required_files ?? []).map((file) => ({ file, source: classSource }));
const headings = [...(cls.required_headings ?? [])];
const seenFiles = new Set(files.map((f) => f.file));
for (const t of traits ?? []) {
const tr = register.trait_requirements?.[t];
if (!tr) continue;
for (const f of tr.required_files ?? []) if (!files.includes(f)) files.push(f);
const traitSource = `trait \`${t}\``;
for (const f of tr.required_files ?? []) {
if (seenFiles.has(f)) continue;
seenFiles.add(f);
files.push({ file: f, source: traitSource });
}
for (const h of tr.required_headings ?? []) if (!headings.includes(h)) headings.push(h);
}
return { files, headings };
@ -460,9 +560,9 @@ export function checkRequiredFiles({ present, klass, traits }, register) {
const { files: required } = requirementsFor(klass, traits, register);
const have = new Set(present ?? []);
const findings = [];
for (const f of required) {
if (!have.has(f)) {
findings.push({ level: 'ERROR', code: 'FILE-MISSING', bucket: 'missing', msg: `missing required file for class \`${klass}\`: ${f}` });
for (const { file, source } of required) {
if (!have.has(file)) {
findings.push({ level: 'ERROR', code: 'FILE-MISSING', bucket: 'missing', msg: `missing required file for ${source}: ${file}` });
}
}
if (findings.length === 0 && required.length > 0) {
@ -475,16 +575,32 @@ export function checkRequiredFiles({ present, klass, traits }, register) {
// on a predictable heading is what agents pattern-match on, and `## Non-goals`
// is the cheapest trust-builder there is: it proves someone thought about the
// boundary, and it stops misuse before it starts.
export function checkHeadings({ readme, klass, traits }, register) {
export function checkHeadings({ readme, klass, traits, name }, register) {
const { headings: required } = requirementsFor(klass, traits, register);
const text = String(readme ?? '');
const present = new Set(
text.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('#')),
);
const aliases = register.heading_aliases?.[name] ?? {};
const findings = [];
for (const h of required) {
if ([...present].some((p) => p.toLowerCase() === h.toLowerCase())) continue;
// Same job `titles` does for a README H1, one requirement over: the
// decision (this repo's readers were declared `nb`, so the contract's
// English wording is the wrong test) is taken in the repo, the bookkeeping
// happens here. Keyed per repo, not per locale — two nb-repos need not
// phrase the same section the same way.
const alias = aliases[h];
if (alias && [...present].some((p) => p.toLowerCase() === alias.toLowerCase())) {
findings.push({
level: 'OK',
code: 'HEADING-ALIAS',
msg: `\`${alias}\` satisfies \`${h}\` — the registered heading alias for \`${name}\``,
});
continue;
}
// Same title, wrong depth: say that, rather than "missing". The contract
// wants a predictable top-level heading because that is what an agent
// pattern-matches on — but the section does exist, and the fix is a
@ -549,6 +665,229 @@ export function checkVersionConsistency({ pluginVersion, readmeBadge, changelogT
return findings;
}
// Version order, not the order git handed the tags over. `git tag --list` sorts
// lexically, where v10.0.0 lands BEFORE v9.0.0 — so reading "newest" off an
// unsorted list picks the wrong tag on precisely the repos with the longest
// release history (repo-mailbox has 27). Numeric triple first; a pre-release
// suffix sorts BELOW the bare release, as semver has it, which is what keeps
// `v0.5.0a2` from outranking `v0.5.0`.
function compareTags(a, b) {
const parts = (s) => {
const m = /^v?(\d+)\.(\d+)\.(\d+)(.*)$/.exec(String(s));
return m ? [Number(m[1]), Number(m[2]), Number(m[3]), m[4]] : [0, 0, 0, String(s)];
};
const [aM, aN, aP, aRest] = parts(a);
const [bM, bN, bP, bRest] = parts(b);
if (aM !== bM) return aM - bM;
if (aN !== bN) return aN - bN;
if (aP !== bP) return aP - bP;
if (aRest === bRest) return 0;
if (aRest === '') return 1;
if (bRest === '') return -1;
return aRest < bRest ? -1 : 1;
}
// 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 a movable tag is a movable pin — this is a supply
// chain property, not a tidiness one.
//
// The two levels come from a measurement, not from taste. Across all 19 clones:
// 155 tags, 14 of them 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, so it is an ERROR. The older ones can only be
// "fixed" by force-moving an already published ref — the exact act this check
// exists to warn about — so they are exposed once, as a count, and never as
// fourteen separate findings. A gate that demands an unsafe remedy is a gate
// that gets switched off.
//
// Read entirely from local git objects: `git for-each-ref` reports the object
// type with no network call, so this costs nothing against the two-call budget.
//
// `tags_lightweight_accepted` is where a decided YES about tag HISTORY lives.
// Without it the WARN below can never be cleared — the only remedy is force-
// moving a published ref, the act the check exists to warn about — so the gate
// would report the same thing forever and make "we decided this" and "nobody
// looked" the same output. That is the `titles` defect, one axis over.
// 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.
// Acceptance reaches history ONLY. The newest tag is the one lightweight tag
// with a safe remedy (`git tag -a -f`), so it cannot be accepted away.
export function checkTagIntegrity({ tagObjects, name }, register) {
const tags = [...(tagObjects ?? [])].sort((a, b) => compareTags(a.name, b.name));
if (tags.length === 0) {
// The VERSION-NONE shape: the check ran, saw every tag there is, and found
// no subject. A repo with no tags has no ref that could be moved — there is
// nothing here to be wrong, which is a verdict, not an absent one.
return [{ level: 'OK', code: 'TAGS-NONE', msg: 'repo has no version tags — no tag exists that could be moved' }];
}
const findings = [];
const newest = tags[tags.length - 1];
const accepted = new Set(register?.tags_lightweight_accepted?.[name] ?? []);
if (!newest.annotated) {
if (accepted.has(newest.name)) {
// Named acceptance, not a standing exemption: only THIS exact tag is
// excused, so a later real release still fires ERROR the moment it
// becomes newest and isn't itself on the list (proven by the sibling
// test below).
findings.push({
level: 'OK',
code: 'TAG-ANNOTATED-ACCEPTED-NEWEST',
msg: `newest tag \`${newest.name}\` is lightweight, but the register accepts it by name as an exception to the newest-tag rule — a safe remedy exists (\`git tag -a -f\`) but costs more than the finding for this specific tag. Any OTHER tag that becomes newest is still judged.`,
});
} else {
findings.push({
level: 'ERROR',
code: 'TAG-ANNOTATED',
bucket: 'broken',
msg: `newest tag \`${newest.name}\` is lightweight — it can be moved to another commit with no record that it ever pointed elsewhere, and the catalog pins releases by tag. Re-cut it annotated: \`git tag -a -f ${newest.name} ${newest.name}^{}\`.`,
});
}
}
const olderLightweight = tags.slice(0, -1).filter((t) => !t.annotated);
const older = olderLightweight.filter((t) => !accepted.has(t.name));
const excused = olderLightweight.filter((t) => accepted.has(t.name));
if (older.length > 0) {
findings.push({
level: 'WARN',
code: 'TAG-ANNOTATED-HISTORY',
bucket: 'weakening',
msg: `${older.length} older lightweight tag(s) (${older.slice(0, 3).map((t) => t.name).join(', ')}${older.length > 3 ? ', …' : ''}) — each is movable without a trace. WARN, not ERROR: the only remedy is force-moving an already published ref, which is the risk itself. Cut every NEW tag annotated (\`git tag -a\`).`,
});
}
// An exemption is a finding, not a deletion — the same rule `readme_desc_match`
// follows. An exception nobody can see reads exactly like a check that
// silently stopped running.
if (excused.length > 0) {
findings.push({
level: 'OK',
code: 'TAG-ANNOTATED-ACCEPTED',
msg: `${excused.length} older lightweight tag(s) (${excused.map((t) => t.name).join(', ')}) are recorded in the register as accepted history — force-moving a published ref is the only remedy, so the operator accepted them rather than rewrite them. Any NEW lightweight tag, and the newest tag, are still judged.`,
});
}
if (findings.length === 0) {
findings.push({ level: 'OK', code: 'TAGS', msg: `all ${tags.length} version tag(s) are annotated — none can be moved without a record` });
}
return findings;
}
// A signed tag is the one thing that ties a released ref to a person rather
// than to 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.
//
// WHY it exists is not the adoption rate. 0 of 158 tags in the org are signed —
// which is exactly what a practice nobody has adopted yet looks like, and
// reasoning from that zero would have the gate ratify the status quo it exists
// to move. The operator's rule (2026-08-13) is the one that decided it: would a
// public AAA+ repository do this? Yes. That is a different question from the one
// that rejected VERSION-DRIFT, where two legitimate conventions genuinely
// coexisted; there is no second convention under which release tags are better
// unattributable.
//
// The measurement decides the SCOPE. Across 20 clones: 158 `v*` tags, 144
// annotated, 14 lightweight, 0 signed, and not one tag dated later than
// 2026-08-12. So the policy is a DATE held in the register: zero findings on the
// day it lands, teeth at each repo's next release. That is how signing is
// adopted in public — never by rewriting published history, which is the same
// unsafe remedy TAG-ANNOTATED refuses to demand.
//
// The date lives in the register and NOT in this file, 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. Absent, the check is a notRun
// SKIP — an operator adding the key turns it into a verdict.
//
// Two exclusions keep one defect from being counted twice under two codes:
// - a LIGHTWEIGHT tag has no tag object to carry a signature, so there is no
// version of it that passes while staying lightweight — TAG-ANNOTATED owns
// it, and the skip names that owner instead of going quiet.
// - a tag cut BEFORE the policy was correct when it was cut.
//
// Read from local git objects, so it costs nothing against the API budget. It
// answers *is it signed* and never *does the forge vouch for it* — that badge
// needs a key registered there, which is a different subject on a different
// surface.
export function checkTagSigned({ tagObjects }, register) {
const from = register?.tags_signed_from;
if (!from) {
return [{
level: 'SKIP',
skip: 'notRun',
code: 'TAG-SIGNED-NOPOLICY',
msg: 'the register records no tag-signing policy (`tags_signed_from`) — whether this org signs its tags is an operator decision, not one the gate makes on its own',
}];
}
const tags = [...(tagObjects ?? [])].sort((a, b) => compareTags(a.name, b.name));
if (tags.length === 0) {
// The TAGS-NONE / VERSION-NONE shape: the check ran, saw every tag there is,
// and found no subject. That is a verdict, not an absent one.
return [{ level: 'OK', code: 'TAG-SIGNED-NONE', msg: 'repo has no version tags — no tag exists that could carry a signature' }];
}
const findings = [];
// ISO dates compare correctly as strings, which also keeps this free of any
// clock reading — the policy boundary is data, never "now".
const inScope = tags.filter((t) => t.annotated && t.date && t.date >= from);
const prePolicy = tags.filter((t) => t.date && t.date < from);
const lightweight = tags.filter((t) => !t.annotated && t.date && t.date >= from);
if (inScope.length > 0) {
const newest = inScope[inScope.length - 1];
if (!newest.signed) {
findings.push({
level: 'ERROR',
code: 'TAG-SIGNED',
bucket: 'weakening',
msg: `newest tag \`${newest.name}\` was cut under the signing policy (from ${from}) but carries no signature — nothing ties it to a signer rather than to whoever could write to the forge, and the catalog pins releases by tag. Re-cut it signed: \`git tag -s -f ${newest.name} ${newest.name}^{}\`.`,
});
}
const older = inScope.slice(0, -1).filter((t) => !t.signed);
if (older.length > 0) {
findings.push({
level: 'WARN',
code: 'TAG-SIGNED-HISTORY',
bucket: 'weakening',
msg: `${older.length} older unsigned tag(s) cut under the policy (${older.slice(0, 3).map((t) => t.name).join(', ')}${older.length > 3 ? ', …' : ''}). WARN, not ERROR: the only remedy is force-moving an already published ref, which is its own risk. Sign every NEW tag (\`git tag -s\`).`,
});
}
}
// An exemption is a finding, not a deletion — the rule `readme_desc_match`
// and TAG-ANNOTATED-ACCEPTED already follow. An exception nobody can see
// reads exactly like a check that silently stopped running.
if (prePolicy.length > 0) {
findings.push({
level: 'OK',
code: 'TAG-SIGNED-PREPOLICY',
msg: `${prePolicy.length} tag(s) predate the signing policy (from ${from}) and are not judged — they were correct when they were cut, and signing them now would mean force-moving published refs. Every tag from ${from} onward is judged.`,
});
}
if (lightweight.length > 0) {
findings.push({
level: 'SKIP',
skip: 'byDesign',
code: 'TAG-SIGNED-LIGHTWEIGHT',
msg: `${lightweight.length} tag(s) cut under the policy are lightweight (${lightweight.map((t) => t.name).join(', ')}) — a lightweight tag has no tag object to carry a signature, so signing is not a remedy it has. TAG-ANNOTATED owns the verdict on these — an ERROR, or an OK if the register names one as an accepted exception; this check declines rather than report one defect twice.`,
});
}
// The affirmative verdict is gated on the JUDGED findings only, never on
// `findings.length`. Gating on the whole list suppressed it the moment a
// pre-policy OK was present — which is every repo in the org, so this OK was
// near-unreachable in practice, and a reader could not tell "signed its new
// tag" from "has cut no tag since the policy". Two different facts, both green.
const judged = findings.some((f) => f.level === 'ERROR' || f.level === 'WARN');
if (!judged) {
findings.push(inScope.length > 0
? { level: 'OK', code: 'TAGS-SIGNED', msg: `all ${inScope.length} tag(s) cut under the policy (from ${from}) are signed` }
: { level: 'OK', code: 'TAGS-SIGNED', msg: `no tag has been cut since the signing policy took effect (${from}) — nothing to judge yet` });
}
return findings;
}
// A static image asserting "tests: 642 passing" is a claim dressed as evidence.
// Version, licence and platform badges assert no run, so they are fine static.
// Bare `status` used to be in this list and caught a self-declared maturity
@ -556,7 +895,202 @@ export function checkVersionConsistency({ pluginVersion, readmeBadge, changelogT
// llm-ingestion-pipeline-security. `build`/`ci`/`passing` already catch the
// run-asserting compounds ("build status", "CI status"), so dropping the bare
// word loses no real detection.
const CLAIM_BADGE = /(tests?|build|ci|coverage|passing)/i;
// Matched WORD by word, never as a substring. `selftest_checks-402` — a count
// of checks that exist, asserting nothing about a run — fired for three
// censuses because `tests?` matched the letters inside "selfTESTs" (org-ops,
// 2026-08-12, on repo-mailbox's dispute). Splitting on every non-alphanumeric
// run, rather than leaning on `\b`, is what keeps the fix from creating the
// opposite defect: shields.io writes a space as `_`, so `\btests\b` would have
// gone quiet on the genuine claim `tests-402_passing`.
const CLAIM_WORD = /^(tests?|build|ci|coverage|passing)$/i;
const claimsARun = (s) => s.split(/[^a-z0-9]+/i).some((w) => CLAIM_WORD.test(w));
// The forge's release page is where a stranger lands when they want a version
// they can name, and it is the one surface refs cannot answer — a release is
// not a ref, so `git ls-remote` has nothing to report. That is why this is the
// THIRD API call and the only new one the acquisition model adds.
//
// Both sides come 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 real finding is a tag that was never pushed (v1.0.0
// local, v0.1.0 published) — a different defect, owned by a different check.
//
// The levels come from a measurement across all 22 registered repos
// (2026-08-12): 4 have no tags at all, 2 tag without ever publishing a release,
// 11 are current, 5 lag their newest tag. Those 2 are why "no releases" is an
// OK and not a finding: nothing in a repo 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 — the same measurement that rejected
// VERSION-DRIFT one check over. Lagging is a WARN rather than an ERROR 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.
export function checkReleaseCurrent({ forgeTagsSelf, releases }) {
if (releases === null || releases === undefined) {
return [{ level: 'SKIP', skip: 'notRun', code: 'RELEASE-UNAVAILABLE', msg: 'forge releases not available — check not run (offline, or the listing failed)' }];
}
// One side missing is not agreement. Refs and releases are acquired over two
// different channels, so either can fail alone.
if (forgeTagsSelf === null || forgeTagsSelf === undefined) {
return [{ level: 'SKIP', skip: 'notRun', code: 'RELEASE-UNAVAILABLE', msg: 'forge refs not readable — cannot tell whether the newest release is the newest tag' }];
}
const tags = [...forgeTagsSelf].sort(compareTags);
if (tags.length === 0) {
// The VERSION-NONE shape: the check ran, saw every tag the forge has, and
// found no subject. Nothing could have been released, so there is nothing
// here to be wrong — a verdict, not an absent one.
return [{ level: 'OK', code: 'RELEASE-NONE', msg: 'no tags on the forge — no release could exist' }];
}
const newestTag = tags[tags.length - 1];
if (releases.length === 0) {
return [{
level: 'OK',
code: 'RELEASE-TAGS-ONLY',
msg: `${tags.length} tag(s) and no release published — this repo tags without publishing releases, which is a convention this gate does not judge`,
}];
}
// Version order, not the order the API handed them over: the releases listing
// sorts by creation time, and a patch cut after a minor would read as newest.
const newestRelease = [...releases].sort(compareTags).pop();
if (compareTags(newestRelease, newestTag) < 0) {
return [{
level: 'WARN',
code: 'RELEASE-STALE',
bucket: 'weakening',
msg: `newest release is \`${newestRelease}\` but the newest tag is \`${newestTag}\` — the release page shows a version older than the code. Publish a release for \`${newestTag}\`.`,
}];
}
return [{
level: 'OK',
code: 'RELEASE-CURRENT',
msg: newestRelease === newestTag
? `newest release \`${newestRelease}\` is the newest tag`
: `newest release \`${newestRelease}\` is ahead of every tag the forge lists`,
}];
}
// A tag that exists only in the operator's clone is a version that exists for
// nobody. 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 it.
// Measured across all 21 registered clones (2026-08-12), exactly one repo is in
// that state — portfolio-optimiser — 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: nobody deliberately keeps a release tag private, the remedy
// (`git push origin <tag>`) is safe and moves no published ref, and the finding
// recurs at every release, not 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, which is the mechanism that gets gates switched off.
export function checkRemoteSync({ tags, forgeTagsSelf }) {
if (forgeTagsSelf === null || forgeTagsSelf === undefined) {
return [{ level: 'SKIP', skip: 'notRun', code: 'REMOTE-SYNC', msg: 'forge refs not readable — cannot tell whether the local tags were ever pushed' }];
}
const onForge = new Set(forgeTagsSelf);
const unpushed = [...(tags ?? [])].filter((t) => !onForge.has(t)).sort(compareTags);
if (unpushed.length === 0) {
return [{
level: 'OK',
code: 'REMOTE-SYNC',
msg: (tags ?? []).length === 0
? 'no local tags — nothing that could be unpushed'
: `all ${tags.length} local tag(s) exist on the forge`,
}];
}
const names = unpushed.map((t) => `\`${t}\``).join(', ');
return [{
level: 'ERROR',
code: 'REMOTE-SYNC',
bucket: 'broken',
msg: `${names} exist${unpushed.length === 1 ? 's' : ''} only in this clone — the forge has no such tag, so the version is unreachable for everyone else. Push it: \`git push origin ${unpushed.join(' ')}\`.`,
}];
}
// The families a clean clone actually runs, read off the corpus rather than
// imagined: npm/pnpm/yarn scripts, `node --test`, a named test file, pytest,
// make, a shell test script, and the `--selftest` flag repo-mailbox ships.
// `npm install` must NOT match — the install block is fenced in every repo in
// the org, and matching it would hand a green line to every repo this check
// exists to find.
const VERIFY_COMMAND = new RegExp([
'(^|\\s)(npm|pnpm|yarn)\\s+(run\\s+\\S*test\\S*|test)\\b',
'(^|\\s)node\\s+--test\\b',
'\\.test\\.(mjs|cjs|js|ts)\\b',
'(^|\\s)(python3?\\s+-m\\s+)?pytest\\b',
'(^|\\s)make\\s+(test|check)\\b',
'--selftest\\b',
'(^|[\\s./])\\S*(test|selftest)\\S*\\.sh\\b',
].join('|'));
// The complement of `stripCode`, and deliberately derived FROM it: the link
// checks need code removed, this one needs exactly what was removed. A second
// hand-rolled fence parser is how two copies of one rule drift apart.
export function codeLines(text) {
const src = String(text ?? '').split('\n');
const stripped = stripCode(text).split('\n');
return src.filter((line, i) => stripped[i] === ''
&& line.trim() !== ''
&& !/^\s*(```|~~~)/.test(line));
}
// There is no CI badge in this org because there is no CI — the published
// substitute, stated in this repo's own README, is one command a stranger can
// run from a clean clone. That SINGLE published stance is what licenses a check
// that fires on a third of the org: VERSION-DRIFT was rejected because twelve
// of the fifteen repos it felled were simply following the other legitimate
// convention, and here there is no other convention. A repo with a runnable
// suite and no documented command is not on a different plan; it is
// undocumented.
//
// The subject is MEASURED, never read off a class. Across all 21 registered
// clones (2026-08-12), 16 have something runnable and 5 do not —
// human-friendly-style, llm-security-commons, playground-design-system,
// portfolio-optimiser-commons and app-creator hold prose, output styles and
// domain packs. Those five span the `plugin`, `shared-asset` and `standalone`
// classes, so any class-level requirement would have failed a correct
// repository somewhere. Nothing to verify is an OK, the RELEASE-NONE shape.
//
// What this check can NEVER do is report that a documented command works — it
// runs nothing. It fells a missing command and nothing else, and the message
// says so, because a green line implying a passing suite is a claim on the
// surface that nobody verified. It also reads only the README and package.json,
// so unlike every check since PIN-DEAD it has no null network input and
// therefore no SKIP at all.
export function checkVerifyCommand({ readme, testScript, testFileCount }) {
const count = Number(testFileCount ?? 0);
if (!testScript && count === 0) {
return [{
level: 'OK',
code: 'VERIFY-NONE',
msg: 'no test script and no tracked test file — nothing here a stranger could run, so no verification command is owed',
}];
}
const found = codeLines(readme).find((l) => VERIFY_COMMAND.test(l));
if (found) {
return [{
level: 'OK',
code: 'VERIFY-COMMAND',
msg: `README shows \`${found.trim()}\` — a stranger has one command to run. This gate does not run it, so this says documented, never passing.`,
}];
}
const have = testScript
? `\`${testScript}\` is defined in package.json`
: `${count} tracked test file(s) exist`;
return [{
level: 'WARN',
code: 'VERIFY-MISSING',
bucket: 'missing',
msg: `${have}, but no README code block shows a command to run them — with no CI badge to fall back on, a stranger has no way to check this repo works. Show the command in a fenced block.`,
}];
}
// Counting badges needs a NARROWER rule than detecting a dishonest one. The
// claim check reads any image, any host, on purpose. Here the opposite error
@ -587,7 +1121,7 @@ export function checkBadges({ readme, present }) {
const linked = linkTarget !== undefined;
const label = `${m[2]} ${m[3]}`;
if (BADGE_URL.test(m[3])) badgeCount++;
if (!CLAIM_BADGE.test(label)) continue;
if (!claimsARun(label)) continue;
if (!linked) {
findings.push({
level: 'WARN',
@ -847,6 +1381,25 @@ function isFixturePath(path) {
.some((seg) => seg === 'test' || seg === 'tests' || seg === 'fixtures' || seg.includes('golden'));
}
// A home directory is what makes a `file:` URL a leak rather than a scheme the
// gate declines to resolve. Anchored on the two roots a real machine path
// starts with; a bare `file:///abs/path.html` placeholder is not one.
//
// WIDENING TO OTHER ROOTS WAS CONSIDERED AND REJECTED FOR NOW — an order
// (`.claude`, 2026-08-18) asked whether `C:\`, `/private/`, `/var/`, `/tmp/`
// and similar roots deserved the same anchor, having measured only the
// `/Users/ktg` case. Measured here across every `.md` file in the 21 of 22
// registered repos with a local clone (`llm-security-commons` absent):
// exactly two REAL leaks exist in the entire corpus, both `/Users/...` in
// `ki-produktivitetsmodell` (already caught — the positive control that
// proves this grep can find one). Every other `file://` hit is a generic
// placeholder (`/abs/path`, `<abs...>`, `${...}`) that anchoring on Users|home
// already declines to flag. Zero occurrences of any other real root, in this
// corpus, on this date — a RELEASE-ASSETS-shaped rejection: not a low rate to
// veto later, an absent subject. Widen this the day a real one appears, not
// before; re-measure rather than trust this count if this comment goes stale.
const FILE_URL_LEAK = /^file:\/\/\/?(Users|home)\//i;
// Relative file links only. Anchor resolution depends on per-renderer heading
// slug rules and is a rabbit hole; external URLs need the network. Both are
// deliberately out — a check that is sometimes wrong teaches people to ignore it.
@ -865,8 +1418,27 @@ export function checkInternalLinks({ files, present }) {
stripCode(text).split('\n').forEach((line, i) => {
for (const m of line.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g)) {
const target = m[1];
// Any scheme at all, not just http — `file:`, `vscode:`, `ftp:` are all
// somebody else's to resolve.
// A `file:` URL naming a real home directory is the one scheme that is
// NOT somebody else's to resolve: it is dead for every reader but its
// author, and it publishes that author's directory layout.
//
// The scheme alone is not the rule. Measured over the corpus, 40 such
// links split 18/22 between a documented convention example — the same
// two lines copy-pasted into nine CLAUDE.md files — and real machine
// paths in three repos. `...` is not a path segment, so a target
// containing `/.../` cannot resolve on ANY machine and is by
// construction an illustration, not a leak.
if (FILE_URL_LEAK.test(target) && !/\/\.\.\.\//.test(target)) {
findings.push({
level: linkLevelFor(path),
code: 'LINK-FILE-URL',
bucket: 'broken',
msg: `${path}:${i + 1}\`${target}\` is a link into a local filesystem: dead for every reader but its author, and it publishes the author's directory layout. Link the repository-relative path, or the published URL.`,
});
continue;
}
// Any other scheme, not just http — `vscode:`, `ftp:` are all somebody
// else's to resolve.
if (/^[a-z][a-z0-9+.-]*:/i.test(target) || /^[#<]/.test(target)) continue;
const clean = target.split('#')[0];
if (!clean) continue;
@ -971,7 +1543,7 @@ export function bucketsOf(findings) {
}
export function classifyRepo(
{ name, files, present, description, pluginVersion, readmeBadge, changelogTop, tags, catalogNames },
{ name, files, present, description, pluginVersion, readmeBadge, changelogTop, tags, tagObjects, catalogNames, forgeTagsByRepo, forgeTagsSelf, releases, testScript, testFileCount },
register,
) {
const klass = register.repos?.[name];
@ -999,7 +1571,8 @@ export function classifyRepo(
...checkFirstScreen({ readme, name, description, klass }, register),
...checkInstallBlock({ readme, name, klass }, register),
...checkInstallTruth({ name, klass, catalogNames }),
...checkHeadings({ readme, klass, traits }, register),
...checkInstallPins({ readme, forgeTagsByRepo }, register),
...checkHeadings({ readme, klass, traits, name }, register),
...checkRequiredFiles({ present, klass, traits }, register),
...checkLinks({ files }, register),
...checkInternalLinks({ files, present }),
@ -1008,6 +1581,11 @@ export function classifyRepo(
...checkReadmeLanguage({ readme, name }, register),
...checkBoilerplate({ files }),
...checkVersionConsistency({ pluginVersion, readmeBadge, changelogTop, tags }),
...checkTagIntegrity({ tagObjects, name }, register),
...checkTagSigned({ tagObjects }, register),
...checkReleaseCurrent({ forgeTagsSelf, releases }),
...checkRemoteSync({ tags, forgeTagsSelf }),
...checkVerifyCommand({ readme, testScript, testFileCount }),
...checkDescription(description, register),
];
@ -1085,6 +1663,25 @@ async function fetchCatalogNames(register) {
}
}
// The third API call. Releases have no git equivalent — `ls-remote` reports
// refs, and a release is not a ref — so this is the one subject the cheaper
// channel cannot cover. Drafts are excluded: a draft is not published, so it is
// not a claim anyone can read. Pre-releases are kept; llm-ingestion-okf's
// v0.5.0a2 is its real newest release. Null on any failure, which reads as SKIP
// rather than as a pass.
async function fetchReleases(register, repo) {
const url = `${register.forge}/api/v1/repos/${register.org}/${repo}/releases?limit=50`;
try {
const res = await fetchWithRetry(url, { headers: { accept: 'application/json' } });
if (!res.ok) return null;
const json = JSON.parse(await res.text());
if (!Array.isArray(json)) return null;
return json.filter((r) => !r.draft).map((r) => r.tag_name).filter(Boolean);
} catch {
return null;
}
}
async function fetchOrgListing(register) {
const url = `${register.forge}/api/v1/orgs/${register.org}/repos?limit=50`;
const res = await fetchWithRetry(url, { headers: { accept: 'application/json' } });
@ -1155,6 +1752,41 @@ function readPackageVersion(dir) {
return null;
}
// The declared way to run this repo's tests, if there is one. Only
// package.json carries it — a plugin manifest has no scripts, and pyproject's
// runner is not a command a stranger can copy.
function readTestScript(dir) {
const full = join(dir, 'package.json');
if (!existsSync(full)) return null;
try {
const scripts = JSON.parse(readFileSync(full, 'utf8')).scripts ?? {};
return scripts.test ? String(scripts.test) : null;
} catch {
return null;
}
}
// Files that are unambiguously EXECUTABLE tests, not merely files living under
// `tests/`. The looser rule counted golden files, fixtures and transcripts —
// portfolio-optimiser's `tests/golden/demo-transcript.stdout` among them — and
// a finding that says "you have tests a stranger cannot run" is false the
// moment its subject is a fixture. Measured: 16 of 21 clones have a real one.
// One alternative per family rather than one regex, because the shell-script
// family needs a boundary the others do not: a bare `test` substring makes
// `latest-release.sh` a test file.
const EXECUTABLE_TEST_FILE = [
/\.test\.(mjs|cjs|js|ts)$/,
/(^|\/)test_[^/]+\.py$/,
/_test\.py$/,
/(^|\/)([^/]*[-_.])?tests?([-_.][^/]*)?\.sh$/,
/(^|\/)tests?\/[^/]*\.sh$/,
/selftest/,
];
export function countTestFiles(tracked) {
return (tracked ?? []).filter((f) => EXECUTABLE_TEST_FILE.some((re) => re.test(f))).length;
}
export function extractBadgeVersion(readmeText) {
const m = /badge\/version-(\d+\.\d+\.\d+)/.exec(readmeText || '');
return m ? m[1] : null;
@ -1185,7 +1817,55 @@ function gitTags(dir) {
}
}
export function inspectRepo(dir, name, register, description, catalogNames = null) {
// `%(objecttype)` is `tag` for an annotated tag and `commit` for a lightweight
// one — the distinction read straight off the local object database, with no
// network call, so tag integrity costs nothing against the two-call budget.
// Refs from the FORGE, over the git protocol — anonymous, and measured not to
// share the API's rate-limit bucket, so it costs nothing against the two-call
// budget. The URL is derived from the register, never from `origin`: at least
// one repo's origin is `ssh://git@…`, which would need the operator's key and
// so would work here and fail for every other reader.
function forgeTags(register, repo) {
const forge = String(register?.forge ?? '').replace(/\/+$/, '');
if (!forge || !register?.org) return null;
try {
const out = execFileSync('git', ['ls-remote', '--tags', `${forge}/${register.org}/${repo}.git`], {
encoding: 'utf8',
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
return out.split('\n')
.map((l) => l.split('\t')[1])
.filter((r) => r && !r.endsWith('^{}'))
.map((r) => r.replace(/^refs\/tags\//, ''));
} catch {
// Unreachable forge leaves this null, which reads as SKIP/notRun — never
// as a pass.
return null;
}
}
// The signature is read with `%(if)%(contents:signature)%(then)…`, not by
// printing the block itself: a PGP or SSH signature is multi-line, and emitting
// it would break a line-oriented parser on exactly the tags the check cares
// about. Git answers the yes/no question directly, so one process still covers
// every tag. `%(creatordate:short)` is the tag's own date for an annotated tag
// and the commit's for a lightweight one — ISO, so the policy comparison is a
// string compare with no clock reading anywhere.
function gitTagObjects(dir) {
try {
const format = '%(objecttype) %(if)%(contents:signature)%(then)signed%(else)unsigned%(end) %(creatordate:short) %(refname:short)';
return execFileSync('git', ['-C', dir, 'for-each-ref', `--format=${format}`, 'refs/tags/v*'], { encoding: 'utf8' })
.split('\n').map((s) => s.trim()).filter(Boolean)
.map((line) => {
const [type, sig, date, ...rest] = line.split(' ');
return { name: rest.join(' '), annotated: type === 'tag', signed: sig === 'signed', date };
});
} catch {
return [];
}
}
export function inspectRepo(dir, name, register, description, catalogNames = null, offline = false, releases = null) {
const tracked = gitFiles(dir);
const present = (tracked ?? []).filter((f) => existsSync(join(dir, f)));
@ -1203,16 +1883,39 @@ export function inspectRepo(dir, name, register, description, catalogNames = nul
let changelog = null;
try { changelog = readFileSync(join(dir, 'CHANGELOG.md'), 'utf8'); } catch { /* absent */ }
// Only the repos this README actually pins are fetched — one ref listing
// each, and nothing at all for the common case of no pins.
// This repo's own refs, from the forge rather than the clone — the side
// RELEASE-CURRENT compares a published release against, and the side
// REMOTE-SYNC will need next. Over the git protocol, so it costs nothing
// against the API budget.
let forgeTagsSelf = null;
let forgeTagsByRepo = null;
if (!offline) {
forgeTagsSelf = forgeTags(register, name);
forgeTagsByRepo = {};
for (const repo of new Set(extractInstallPins(readme, register).map((p) => p.repo))) {
const t = forgeTags(register, repo);
if (t) forgeTagsByRepo[repo] = t;
}
}
return classifyRepo({
name,
files,
present,
description,
forgeTagsByRepo,
pluginVersion: readPackageVersion(dir),
readmeBadge: extractBadgeVersion(readme),
changelogTop: changelog === null ? null : extractChangelogTop(changelog),
tags: gitTags(dir),
tagObjects: gitTagObjects(dir),
testScript: readTestScript(dir),
testFileCount: countTestFiles(tracked),
catalogNames,
forgeTagsSelf,
releases,
}, register);
}
@ -1321,8 +2024,10 @@ async function main(argv) {
let description = null;
let catalogNames = null;
let releases = null;
if (!argv.includes('--offline')) {
catalogNames = await fetchCatalogNames(register);
releases = await fetchReleases(register, name);
try {
const listing = await fetchOrgListing(register);
const row = listing.find((r) => r.name === name);
@ -1333,7 +2038,7 @@ async function main(argv) {
}
}
const result = inspectRepo(dir, name, register, description, catalogNames);
const result = inspectRepo(dir, name, register, description, catalogNames, argv.includes('--offline'), releases);
const engineVersion = readEngineVersion();
const engineCommit = readEngineCommit();

View file

@ -7,7 +7,10 @@
// They are the reason this gate has three outcomes instead of a boolean.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readFileSync, mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
countCodepoints,
normalizeRepoRef,
@ -21,6 +24,10 @@ import {
checkVersionConsistency,
checkHeadings,
checkBadges,
checkTagIntegrity,
checkTagSigned,
checkInstallPins,
extractInstallPins,
checkReadmeLanguage,
checkBoilerplate,
checkLicenseClaim,
@ -38,6 +45,12 @@ import {
loadRegister,
groupSkips,
skipsOf,
checkReleaseCurrent,
checkRemoteSync,
checkVerifyCommand,
countTestFiles,
codeLines,
inspectRepo,
} from './repo-standard-check.mjs';
const REGISTER = {
@ -757,6 +770,549 @@ test('org-profile requires no headings at all', () => {
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
// The Non-goals contract is written in English and matched literally, which
// cannot be satisfied by a repo whose readers were declared `nb` — the fix is
// per-repo, not per-locale, because two nb-repos need not phrase the same
// section the same way. Same shape as `titles`: the decision is taken in the
// repo, the bookkeeping happens in the register.
test('a registered heading alias satisfies a required heading for a different-language reader', () => {
const aliasRegister = {
...REGISTER,
heading_aliases: { 'ki-produktivitetsmodell': { '## Non-goals': '## Virkeområde og forbehold' } },
};
const readme = '# x\n## Virkeområde og forbehold\n';
const f = checkHeadings({ readme, klass: 'shared-asset', name: 'ki-produktivitetsmodell' }, aliasRegister);
assert.equal(f.some((x) => x.level === 'ERROR'), false);
assert.equal(f.some((x) => x.code === 'HEADING-ALIAS'), true);
// The alias is keyed per-repo: a different repo with the same Norwegian
// heading still misses the literal `## Non-goals`.
const other = checkHeadings({ readme, klass: 'shared-asset', name: 'some-other-repo' }, aliasRegister);
assert.equal(other.some((x) => x.code === 'HEADING-MISSING'), true);
});
// ------------------------------------------------------------- file: URL links
// A `file:///Users/ktg/...` link is dead for every reader but its author, and
// it publishes the author's directory layout. The scheme alone is NOT the rule:
// measured across the corpus, 40 such links split 18/22 between a documented
// convention example (`[Brief](file:///Users/ktg/.../brief.html)`, the same two
// lines copy-pasted into nine CLAUDE.md files) and real machine paths (22, in
// three repos). Firing on the scheme would have been wrong 45% of the time on
// the first run — the ratio that gets a gate switched off.
//
// The discriminator is not tuned to this corpus: `...` is not a real path
// segment, so a path containing `/.../` cannot resolve on ANY machine and is by
// construction an illustration.
test('a file: URL naming a real home directory is a finding', () => {
const files = { 'README.md': '[my notes](file:///Users/ktg/repos/x/notes.md)' };
const f = checkInternalLinks({ files, present: ['README.md'] });
const hit = f.find((x) => x.code === 'LINK-FILE-URL');
assert.equal(hit.level, 'ERROR');
assert.equal(hit.bucket, 'broken');
assert.match(hit.msg, /README\.md:1/);
});
test('an elided file: path is an illustration, not a leak — it resolves nowhere by construction', () => {
const files = { 'README.md': '[Brief](file:///Users/ktg/.../brief.html)' };
assert.equal(checkInternalLinks({ files, present: ['README.md'] }).some((x) => x.code === 'LINK-FILE-URL'), false);
});
test('a placeholder file: path is not a leak either', () => {
const files = { 'README.md': '[Open in browser](file:///abs/path.html)' };
assert.equal(checkInternalLinks({ files, present: ['README.md'] }).some((x) => x.code === 'LINK-FILE-URL'), false);
});
// Same reader rule as every other link: the root is the shop window, below it
// live session plans and agent working files.
test('a file: URL below the root is a WARN, not an ERROR', () => {
const files = { 'docs/plan.md': '[notes](file:///home/ktg/notes.md)' };
const f = checkInternalLinks({ files, present: ['docs/plan.md'] });
assert.equal(f.find((x) => x.code === 'LINK-FILE-URL').level, 'WARN');
});
test('other schemes stay somebody else\'s to resolve', () => {
const files = { 'README.md': '[spec](https://example.com/x) [editor](vscode://file/x)' };
assert.equal(checkInternalLinks({ files, present: ['README.md'] }).some((x) => x.code === 'LINK-FILE-URL'), false);
});
// ------------------------------------------------- I/O shell: file loading
//
// Every test above feeds `files` to checkInternalLinks directly — deliberate,
// per the file header: the I/O shell is exercised live, not unit-tested. One
// exception, here: an order (`.claude`, 2026-08-18) diagnosed the file://
// rule as dead because it believed inspectRepo only ever loads README.md —
// wrong (git blame: every tracked .md file, since 2026-07-27, 816ba97) — but
// its point about the TEST SUITE stood: feeding `files` by hand is exactly
// the shortcut that would let every test above stay green while a real
// narrowing of inspectRepo's `.filter((p) => p.endsWith('.md'))` silently
// killed the rule in production. These two go through the real loading path.
function tempGitRepo(files) {
const dir = mkdtempSync(join(tmpdir(), 'repo-standard-io-'));
execFileSync('git', ['init', '-q'], { cwd: dir });
for (const [name, content] of Object.entries(files)) {
const path = join(dir, name);
mkdirSync(join(path, '..'), { recursive: true });
writeFileSync(path, content);
}
execFileSync('git', ['add', '-A'], { cwd: dir });
return dir;
}
test('inspectRepo scans a file: leak in a non-README markdown file through the ordinary loading path', () => {
const dir = tempGitRepo({
'README.md': '# test\n',
'docs/plan.md': '[notes](file:///Users/ktg/repos/x/notes.md)\n',
});
try {
const result = inspectRepo(dir, 'llm-ingestion-pipeline-security', REGISTER, null, null, true);
const hit = result.findings.find((f) => f.code === 'LINK-FILE-URL');
assert.ok(hit, 'LINK-FILE-URL did not fire for a leak outside README.md');
assert.match(hit.msg, /docs\/plan\.md:1/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('inspectRepo raises nothing when no file: leak exists anywhere', () => {
const dir = tempGitRepo({
'README.md': '# test\n',
'docs/plan.md': 'no links here\n',
});
try {
const result = inspectRepo(dir, 'llm-ingestion-pipeline-security', REGISTER, null, null, true);
assert.equal(result.findings.some((f) => f.code === 'LINK-FILE-URL'), false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
// -------------------------------------------------------------- tag integrity
// A lightweight tag is a branch-like ref: it can be moved to a different commit
// with no record that it ever pointed elsewhere. The catalog pins every plugin
// to `ref: vX.Y.Z`, so a movable tag is a movable pin.
//
// Measured across all 19 clones before writing the rule: 155 tags, of which 14
// are lightweight — 8 in catalog, 5 in okf, 1 in guard. Exactly ONE repo has a
// lightweight NEWEST tag. That split is the rule: the newest tag is what a
// consumer resolves today and what an operator can re-cut, so it is an ERROR;
// the older ones can only be "fixed" by force-moving a published ref, which is
// the very risk this check exists to name, so they are exposed as one WARN and
// never as fourteen.
test('a lightweight newest tag is an ERROR — a movable tag is a movable pin', () => {
const f = checkTagIntegrity({ tagObjects: [{ name: 'v1.0.0', annotated: true }, { name: 'v1.1.0', annotated: false }] });
const hit = f.find((x) => x.code === 'TAG-ANNOTATED');
assert.equal(hit.level, 'ERROR');
assert.equal(hit.bucket, 'broken');
assert.match(hit.msg, /v1\.1\.0/);
});
test('all-annotated tags are OK', () => {
const f = checkTagIntegrity({ tagObjects: [{ name: 'v1.0.0', annotated: true }, { name: 'v1.1.0', annotated: true }] });
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
assert.equal(f.some((x) => x.code === 'TAGS'), true);
});
test('older lightweight tags are ONE aggregated WARN, never one finding per tag', () => {
const tagObjects = [
{ name: 'v0.1.0', annotated: false },
{ name: 'v0.2.0', annotated: false },
{ name: 'v0.3.0', annotated: false },
{ name: 'v1.0.0', annotated: true },
];
const f = checkTagIntegrity({ tagObjects });
const warns = f.filter((x) => x.code === 'TAG-ANNOTATED-HISTORY');
assert.equal(warns.length, 1);
assert.equal(warns[0].level, 'WARN');
assert.match(warns[0].msg, /3/);
assert.equal(f.some((x) => x.level === 'ERROR'), false);
});
// Same shape as VERSION-NONE: the check ran, saw every tag there is, and found
// no subject. Nothing here can be wrong, so it is a verdict — not a skip.
test('a repo with no tags has nothing to judge — OK, not SKIP', () => {
const f = checkTagIntegrity({ tagObjects: [] });
assert.equal(f.length, 1);
assert.equal(f[0].level, 'OK');
assert.equal(f[0].code, 'TAGS-NONE');
});
// ---------------------------------------- accepted lightweight tag history
//
// The WARN above can never be cleared: the only remedy is force-moving an
// already published ref, which is the act the check warns about. Left there,
// it is the `titles` defect again — the gate cannot tell "we decided this" from
// "nobody has looked". The register records the decision, keyed on TAG NAME
// (a count goes silently wrong the moment one tag is re-cut and another takes
// its place).
const ACCEPT_REG = { tags_lightweight_accepted: { alpha: ['v0.1.0', 'v0.2.0'] } };
test('an accepted lightweight tag emits an OK naming it — an exemption is a finding, not a deletion', () => {
const tagObjects = [
{ name: 'v0.1.0', annotated: false },
{ name: 'v0.2.0', annotated: false },
{ name: 'v1.0.0', annotated: true },
];
const f = checkTagIntegrity({ tagObjects, name: 'alpha' }, ACCEPT_REG);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-HISTORY'), false);
const ok = f.find((x) => x.code === 'TAG-ANNOTATED-ACCEPTED');
assert.equal(ok.level, 'OK');
assert.match(ok.msg, /v0\.1\.0/);
assert.match(ok.msg, /v0\.2\.0/);
});
test('an unaccepted lightweight tag still fires, and the WARN counts only the unaccepted', () => {
const tagObjects = [
{ name: 'v0.1.0', annotated: false }, // accepted
{ name: 'v0.2.0', annotated: false }, // accepted
{ name: 'v0.3.0', annotated: false }, // NOT accepted
{ name: 'v1.0.0', annotated: true },
];
const f = checkTagIntegrity({ tagObjects, name: 'alpha' }, ACCEPT_REG);
const warn = f.find((x) => x.code === 'TAG-ANNOTATED-HISTORY');
assert.equal(warn.level, 'WARN');
assert.match(warn.msg, /v0\.3\.0/);
assert.equal(/v0\.1\.0/.test(warn.msg), false);
assert.match(warn.msg, /\b1 older/);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-ACCEPTED'), true);
});
// The newest tag is what a consumer resolves today and what an operator can
// re-cut at no cost, so acceptance is never inherited from a repo merely
// having OTHER accepted entries — only an exact name match on THIS tag excuses
// it (the exception directly below).
test('an unaccepted newest lightweight tag is still an ERROR, even when the register has other entries for this repo', () => {
const reg = { tags_lightweight_accepted: { alpha: ['v9.9.9'] } };
const f = checkTagIntegrity({ tagObjects: [{ name: 'v1.0.0', annotated: true }, { name: 'v2.0.0', annotated: false }], name: 'alpha' }, reg);
const hit = f.find((x) => x.code === 'TAG-ANNOTATED');
assert.equal(hit.level, 'ERROR');
});
// A tag that sorts "newest" by version string without being the newest
// RELEASE — a monorepo-era tag predating a split, consumed by nothing — has a
// real safe remedy (`git tag -a -f`, same commit) that still costs more than
// the finding when nothing resolves it. The register names the tag by EXACT
// NAME, never "this repo's newest is always excused" — caught the moment
// `ktg-plugin-marketplace v7.7.2` was found dead weight: the register had
// accepted it since 2026-08-14 (repos.json `tags_lightweight_accepted`) but
// `accepted` was only ever consulted for tags OLDER than newest, so the
// recorded decision could never take effect. Reported by the catalog
// (coord, 2026-08-17): measured against the real register and real tag data,
// exactly two findings — ERROR TAG-ANNOTATED on the (then-)inert acceptance,
// OK TAG-ANNOTATED-ACCEPTED for the 7 older accepted tags.
test('the newest lightweight tag IS excused when the register names it exactly, with a distinct OK code', () => {
const reg = { tags_lightweight_accepted: { alpha: ['v2.0.0'] } };
const f = checkTagIntegrity({ tagObjects: [{ name: 'v1.0.0', annotated: true }, { name: 'v2.0.0', annotated: false }], name: 'alpha' }, reg);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED'), false);
const ok = f.find((x) => x.code === 'TAG-ANNOTATED-ACCEPTED-NEWEST');
assert.equal(ok.level, 'OK');
assert.match(ok.msg, /v2\.0\.0/);
});
// Acceptance names ONE tag, not a standing exemption for "whatever is newest".
// A real new tag cut after the accepted one is still judged the moment it
// becomes newest and isn't itself on the list — exactly what the register's
// own comment promises: "a new lightweight newest tag is not on this list and
// fires ERROR".
test('a NEW lightweight tag cut after an accepted newest is still judged', () => {
const reg = { tags_lightweight_accepted: { alpha: ['v2.0.0'] } };
const f = checkTagIntegrity({ tagObjects: [
{ name: 'v1.0.0', annotated: true },
{ name: 'v2.0.0', annotated: false },
{ name: 'v3.0.0', annotated: false },
], name: 'alpha' }, reg);
const hit = f.find((x) => x.code === 'TAG-ANNOTATED');
assert.equal(hit.level, 'ERROR');
assert.match(hit.msg, /v3\.0\.0/);
});
test('acceptance is per repo — a name accepted for one repo does not excuse another', () => {
const tagObjects = [{ name: 'v0.1.0', annotated: false }, { name: 'v1.0.0', annotated: true }];
const f = checkTagIntegrity({ tagObjects, name: 'beta' }, ACCEPT_REG);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-HISTORY'), true);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-ACCEPTED'), false);
});
test('no register entry leaves the behaviour exactly as it was', () => {
const tagObjects = [{ name: 'v0.1.0', annotated: false }, { name: 'v1.0.0', annotated: true }];
const f = checkTagIntegrity({ tagObjects, name: 'alpha' }, {});
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-HISTORY'), true);
});
// The newest tag is decided by version order, not by the order git happened to
// hand them over. `git tag --list` sorts lexically, where v10.0.0 sorts BEFORE
// v9.0.0 — reading "newest" off an unsorted list would judge the wrong tag on
// exactly the repos with the longest release history.
test('newest is the highest version, not the last element of an unsorted list', () => {
const f = checkTagIntegrity({ tagObjects: [{ name: 'v10.0.0', annotated: true }, { name: 'v9.0.0', annotated: false }] });
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED'), false);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-HISTORY'), true);
});
// ------------------------------------------------------------- tag signing
//
// Built on an operator decision (2026-08-13): the org signs tags FORWARD. The
// decision rule was not the adoption rate — it was "would a public AAA+ repo do
// this", which is a yes. 0 of 158 signed is what you expect of a practice not
// yet adopted, and letting that zero veto the check would make the gate ratify
// the status quo it exists to move.
//
// The measurement decides the SCOPE, not the yes. Across 20 clones: 158 `v*`
// tags, 144 annotated, 14 lightweight, 0 signed, and NOT ONE tag dated after the
// decision. So a policy DATE fires zero findings on day one and acquires teeth
// at each repo's next release — which is how signing is actually adopted, never
// by rewriting published history.
//
// Two exclusions keep this from double-counting a defect that already has an
// owner: a lightweight tag has no tag object to carry a signature at all
// (TAG-ANNOTATED owns it), and a tag predating the policy was correct when it
// was cut.
const SIGN_REG = { tags_signed_from: '2026-08-13' };
test('an unsigned newest tag cut under the policy is an ERROR', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v1.0.0', annotated: true, signed: true, date: '2026-08-14' },
{ name: 'v1.1.0', annotated: true, signed: false, date: '2026-08-15' },
] }, SIGN_REG);
const hit = f.find((x) => x.code === 'TAG-SIGNED');
assert.equal(hit.level, 'ERROR');
assert.equal(hit.bucket, 'weakening');
assert.match(hit.msg, /v1\.1\.0/);
});
test('all in-scope tags signed is an OK', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v1.0.0', annotated: true, signed: true, date: '2026-08-14' },
{ name: 'v1.1.0', annotated: true, signed: true, date: '2026-08-15' },
] }, SIGN_REG);
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
assert.equal(f.some((x) => x.code === 'TAGS-SIGNED'), true);
});
// The same split TAG-ANNOTATED makes, for the same reason: the newest can be
// re-cut, the older ones can only be "fixed" by force-moving a published ref.
test('older unsigned in-scope tags are ONE aggregated WARN, never one per tag', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v1.0.0', annotated: true, signed: false, date: '2026-08-14' },
{ name: 'v1.1.0', annotated: true, signed: false, date: '2026-08-15' },
{ name: 'v1.2.0', annotated: true, signed: true, date: '2026-08-16' },
] }, SIGN_REG);
const warns = f.filter((x) => x.code === 'TAG-SIGNED-HISTORY');
assert.equal(warns.length, 1);
assert.equal(warns[0].level, 'WARN');
assert.match(warns[0].msg, /2/);
assert.equal(f.some((x) => x.level === 'ERROR'), false);
});
// This is the test that keeps the check from failing 20 correct repositories on
// the day the policy lands. Every tag in the corpus predates it.
test('tags predating the policy are never judged — and say so as an OK, not silence', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v0.9.0', annotated: true, signed: false, date: '2026-08-09' },
{ name: 'v1.0.0', annotated: true, signed: false, date: '2026-08-12' },
] }, SIGN_REG);
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
const ok = f.find((x) => x.code === 'TAG-SIGNED-PREPOLICY');
assert.equal(ok.level, 'OK');
assert.match(ok.msg, /2/);
assert.match(ok.msg, /2026-08-13/);
});
// Caught by dogfooding the v0.11.0 release: the affirmative verdict was
// suppressed whenever ANY pre-policy tag existed — which is every repo in the
// org, so `TAGS-SIGNED` would have been near-unreachable in practice. The
// reader could then not tell "this repo signed its new tag" from "this repo has
// cut no tag since the policy". Both are OK-status, and they are not the same
// fact. The exemption OK must not silence the judged one.
test('the affirmative OK survives alongside the pre-policy OK — they are two different facts', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v0.9.0', annotated: true, signed: false, date: '2026-08-09' },
{ name: 'v1.0.0', annotated: true, signed: true, date: '2026-08-14' },
] }, SIGN_REG);
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
assert.equal(f.some((x) => x.code === 'TAG-SIGNED-PREPOLICY'), true);
const ok = f.find((x) => x.code === 'TAGS-SIGNED');
assert.equal(ok.level, 'OK');
assert.match(ok.msg, /\b1 tag/);
});
// The other half of the same distinction: nothing cut since the policy is a
// DIFFERENT sentence from "what was cut is signed", and it must not borrow the
// affirmative one's wording.
test('a repo with only pre-policy tags says nothing has been cut yet, not that anything passed', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v0.9.0', annotated: true, signed: false, date: '2026-08-09' },
] }, SIGN_REG);
const ok = f.find((x) => x.code === 'TAGS-SIGNED');
assert.match(ok.msg, /no tag has been cut/);
});
test('a tag cut exactly ON the policy date is in scope — the policy starts that day', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v1.0.0', annotated: true, signed: false, date: '2026-08-13' },
] }, SIGN_REG);
assert.equal(f.find((x) => x.code === 'TAG-SIGNED').level, 'ERROR');
});
// A lightweight tag has no tag object, so it cannot carry a signature — there
// is no version of it that passes this check while staying lightweight. Firing
// here would report ONE defect twice under two codes; TAG-ANNOTATED already
// owns it, and the skip names that owner rather than going quiet.
test('a lightweight tag is a byDesign SKIP that names TAG-ANNOTATED as the owner', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v1.0.0', annotated: false, signed: false, date: '2026-08-14' },
] }, SIGN_REG);
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
const skip = f.find((x) => x.code === 'TAG-SIGNED-LIGHTWEIGHT');
assert.equal(skip.level, 'SKIP');
assert.equal(skip.skip, 'byDesign');
assert.match(skip.msg, /TAG-ANNOTATED/);
});
// The engine must hold no org decision of its own — the same rule that keeps
// the org-profile exemption a register flag rather than `if (klass === …)`.
// notRun, not byDesign: an operator adding the key turns this into a verdict.
test('no policy in the register is a notRun SKIP — the engine never assumes the org signs', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v1.0.0', annotated: true, signed: false, date: '2026-08-14' },
] }, {});
assert.equal(f.length, 1);
assert.equal(f[0].level, 'SKIP');
assert.equal(f[0].skip, 'notRun');
assert.equal(f[0].code, 'TAG-SIGNED-NOPOLICY');
});
test('a repo with no tags has nothing to judge — OK, not SKIP', () => {
const f = checkTagSigned({ tagObjects: [] }, SIGN_REG);
assert.equal(f.length, 1);
assert.equal(f[0].level, 'OK');
assert.equal(f[0].code, 'TAG-SIGNED-NONE');
});
// An undatable tag cannot be PROVEN to fall under the policy, and a gate that
// fails a correct repository on a missing field is the mechanism that gets
// gates switched off. Absence falls to the safe side here — unlike an untagged
// SKIP, where the loud side is the safe one.
test('a tag with no readable date is out of scope, never assumed post-policy', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v1.0.0', annotated: true, signed: false },
] }, SIGN_REG);
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
});
// CLAUDE.md fixes this scope in advance: `git cat-file tag` reads the signature
// block from the clone, so the check answers *is it signed* and must never
// claim the forge vouches for it — that needs a key registered there.
test('no finding claims the forge verified anything', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v1.0.0', annotated: true, signed: false, date: '2026-08-14' },
{ name: 'v1.1.0', annotated: true, signed: false, date: '2026-08-15' },
] }, SIGN_REG);
for (const x of f) assert.equal(/verified|vouch|trusted|valid signature/i.test(x.msg), false);
});
test('newest in scope is the highest version, not the last element of an unsorted list', () => {
const f = checkTagSigned({ tagObjects: [
{ name: 'v10.0.0', annotated: true, signed: true, date: '2026-08-14' },
{ name: 'v9.0.0', annotated: true, signed: false, date: '2026-08-15' },
] }, SIGN_REG);
assert.equal(f.some((x) => x.code === 'TAG-SIGNED'), false);
assert.equal(f.some((x) => x.code === 'TAG-SIGNED-HISTORY'), true);
});
// ---------------------------------------------------------------- dead pins
//
// Reported by org-ops (census 08) and re-measured here against the FORGE, not
// the clones: 3 pins in the org, 1 dead —
// `llm-ingestion-pipeline-security` pins ITSELF to `@v0.7.0`, which does not
// exist (newest is v0.6.1). ERROR, not WARN: a dead documentation link costs a
// stranger a 404, a dead pin costs them the one install command that was
// supposed to work.
const PIN_REG = { org: 'open', forge: 'https://git.fromaitochitta.com' };
test('an install pin is extracted with its repo and ref', () => {
const readme = '```bash\npip install "guard @ git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git@v0.7.0"\n```';
const pins = extractInstallPins(readme, PIN_REG);
assert.equal(pins.length, 1);
assert.equal(pins[0].repo, 'llm-ingestion-pipeline-security');
assert.equal(pins[0].ref, 'v0.7.0');
});
test('a pin whose ref is not a tag on the forge is an ERROR', () => {
const readme = 'pip install "g @ git+https://git.fromaitochitta.com/open/guard-repo.git@v0.7.0"';
const f = checkInstallPins({ readme, forgeTagsByRepo: { 'guard-repo': ['v0.6.0', 'v0.6.1'] } }, PIN_REG);
const hit = f.find((x) => x.code === 'PIN-DEAD');
assert.equal(hit.level, 'ERROR');
assert.equal(hit.bucket, 'broken');
assert.match(hit.msg, /v0\.7\.0/);
assert.match(hit.msg, /v0\.6\.1/); // names what DOES exist, so the remedy is obvious
});
test('a pin whose ref resolves is OK', () => {
const readme = 'pip install "g @ git+https://git.fromaitochitta.com/open/guard-repo.git@v0.6.1"';
const f = checkInstallPins({ readme, forgeTagsByRepo: { 'guard-repo': ['v0.6.0', 'v0.6.1'] } }, PIN_REG);
assert.equal(f.some((x) => x.level === 'ERROR'), false);
assert.equal(f.some((x) => x.code === 'PINS'), true);
});
// `ls-remote --tags` cannot answer a branch or a sha, and a branch pin is a
// different weakness (an unpinned install), not a dead one. This check can
// never produce a verdict on it, so the skip is byDesign — nobody has an
// action that would turn it into one.
test('a pin at a branch or a sha is a byDesign skip, never a dead pin', () => {
const readme = 'pip install "g @ git+https://git.fromaitochitta.com/open/guard-repo.git@main"';
const f = checkInstallPins({ readme, forgeTagsByRepo: { 'guard-repo': ['v0.6.1'] } }, PIN_REG);
const hit = f.find((x) => x.code === 'PIN-NOT-A-TAG');
assert.equal(hit.level, 'SKIP');
assert.equal(hit.skip, 'byDesign');
});
test('offline leaves the pin un-judged, and says so as notRun', () => {
const readme = 'pip install "g @ git+https://git.fromaitochitta.com/open/guard-repo.git@v0.7.0"';
const f = checkInstallPins({ readme, forgeTagsByRepo: null }, PIN_REG);
const hit = f.find((x) => x.code === 'PIN-UNAVAILABLE');
assert.equal(hit.level, 'SKIP');
assert.equal(hit.skip, 'notRun');
});
// Caught by the corpus sweep, not by a unit test: 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 checked. SKIP is never a pass.
test('an unverified pin never produces an OK claiming it resolves', () => {
const readme = 'pip install "g @ git+https://git.fromaitochitta.com/open/guard-repo.git@v0.7.0"';
const f = checkInstallPins({ readme, forgeTagsByRepo: null }, PIN_REG);
assert.equal(f.some((x) => x.code === 'PINS'), false);
assert.equal(f.some((x) => x.level === 'OK'), false);
});
test('the OK counts only the pins actually verified', () => {
const readme = [
'pip install "a @ git+https://git.fromaitochitta.com/open/alpha.git@v1.0.0"',
'pip install "b @ git+https://git.fromaitochitta.com/open/beta.git@v2.0.0"',
].join('\n');
const f = checkInstallPins({ readme, forgeTagsByRepo: { alpha: ['v1.0.0'] } }, PIN_REG);
const ok = f.find((x) => x.code === 'PINS');
assert.match(ok.msg, /1 of 2/);
assert.equal(f.some((x) => x.code === 'PIN-UNAVAILABLE'), true);
});
test('a pin at somebody elses forge is not ours to judge', () => {
const readme = 'pip install "x @ git+https://github.com/other/thing.git@v9.9.9"';
const f = checkInstallPins({ readme, forgeTagsByRepo: {} }, PIN_REG);
assert.equal(f.some((x) => x.code === 'PIN-DEAD'), false);
assert.equal(extractInstallPins(readme, PIN_REG).length, 0);
});
// The VERSION-NONE shape: the check ran, read the whole README, and found no
// pin. Nothing here can be wrong — that is a verdict, not an absent one.
test('a README with no pins has nothing to judge — OK, not SKIP', () => {
const f = checkInstallPins({ readme: '# hello\n\nno install pins here', forgeTagsByRepo: {} }, PIN_REG);
assert.equal(f.length, 1);
assert.equal(f[0].level, 'OK');
assert.equal(f[0].code, 'PINS-NONE');
});
// ------------------------------------------------------------ badge honesty
test('a static badge asserting test or build status is a finding', () => {
@ -824,6 +1380,28 @@ test('a build- or CI-status badge is still caught — only bare "status" was too
assert.equal(ci.some((x) => x.code === 'BADGE-STATIC-CLAIM'), true);
});
// Reported by org-ops (coord, 2026-08-12) on behalf of repo-mailbox, which
// carried this WARN through censuses 03, 05 and 06 and disputed it every time.
// Measured here before changing anything: `selftest_checks-402` fired because
// `tests?` matched the SUBSTRING inside "selfTESTs" — a count of checks that
// exist, not an assertion that any of them passed. The rule read a claim word
// wherever the letters appeared, so it also fired on the URL slug alone: their
// proposed fix of renaming the visible label to "Checks" was measured NOT to
// clear it. Only a word-level match does.
test('a claim word inside a longer word is not a claim — `selftest` is not `tests`', () => {
const f = checkBadges({ readme: '![Selftest checks](https://img.shields.io/badge/selftest_checks-402-blue)' });
assert.equal(f.some((x) => x.code === 'BADGE-STATIC-CLAIM'), false);
});
// The separator matters as much as the boundary: shields.io writes a space as
// `_`, so a claim word can arrive with word characters on both sides. Matching
// on `\b` alone would have gone quiet on a real claim while fixing the false
// one — the worse direction of the same defect.
test('an underscore-separated claim word is still a claim — `402_passing`', () => {
const f = checkBadges({ readme: '![Tests](https://img.shields.io/badge/tests-402_passing-green)' });
assert.equal(f.some((x) => x.code === 'BADGE-STATIC-CLAIM'), true);
});
// ----------------------------------------------------------- badge crowding
// Trockman et al., ICSE 2018 (n=294,941 npm packages) measured a non-linear
@ -1017,6 +1595,19 @@ test('a repo without the security trait owes no SECURITY.md', () => {
assert.equal(f.some((x) => x.msg.includes('SECURITY.md')), false);
});
test('a trait-required file is missing, FILE-MISSING names the trait, not the class', () => {
const f = checkRequiredFiles({ present: ['README.md', 'LICENSE'], klass: 'standalone', traits: ['security'] }, REGISTER);
const finding = f.find((x) => x.code === 'FILE-MISSING' && x.msg.includes('SECURITY.md'));
assert.equal(finding.msg.includes('trait `security`'), true);
assert.equal(finding.msg.includes('class `standalone`'), false);
});
test('a class-required file is missing, FILE-MISSING still names the class', () => {
const f = checkRequiredFiles({ present: [], klass: 'standalone' }, REGISTER);
const finding = f.find((x) => x.code === 'FILE-MISSING' && x.msg.includes('LICENSE'));
assert.equal(finding.msg.includes('class `standalone`'), true);
});
test('the security trait requires limitations to be stated', () => {
const f = checkHeadings({ readme: '# x\n## Install\n## Non-goals\n', klass: 'standalone', traits: ['security'] }, REGISTER);
assert.equal(f.some((x) => x.code === 'HEADING-MISSING' && x.msg.includes('Known limitations')), true);
@ -1661,9 +2252,14 @@ test('no version claimed anywhere is nothing to judge, not a skip', () => {
// The rename is the point, not cosmetics: `-UNAVAILABLE` is this engine's
// naming for a `notRun` skip (`DESC-UNAVAILABLE`, `INSTALL-TRUTH`). One
// OK-level `-UNAVAILABLE` would mislead exactly the reader this change serves.
// Scoped to the EMISSION form (`code: '…'`) on purpose. A bare
// `src.includes('VERSION-UNAVAILABLE')` also passes today, and is wrong: it
// would fail the suite the moment someone writes the historical name into a
// comment explaining why the code was renamed — which is exactly the
// archaeology this repo wants kept next to the code.
test('the un-runnable version codes keep the -UNAVAILABLE naming', () => {
const src = readFileSync(new URL('./repo-standard-check.mjs', import.meta.url), 'utf8');
assert.ok(!src.includes('VERSION-UNAVAILABLE'), 'VERSION-UNAVAILABLE was renamed to VERSION-NONE');
assert.ok(!/code:\s*'VERSION-UNAVAILABLE'/.test(src), 'VERSION-UNAVAILABLE was renamed to VERSION-NONE');
});
test('an unreachable catalog is un-runnable — a second run resolves it', () => {
@ -1714,3 +2310,345 @@ test('headerLine still prints the pre-0.8.0 line for a result with no skip split
const line = headerLine({ name: 'okr', klass: 'plugin', traits: [], status: 'OK', notChecked: 1 }, '0.8.0');
assert.match(line, /1 not checked/);
});
// ---------------------------------------------------------- RELEASE-CURRENT
//
// Levels come from the corpus, not from taste. Measured 2026-08-12 across all
// 22 registered repos (forge releases API vs `git ls-remote --tags`):
// 4 have no tags at all, 2 have tags and publish no releases,
// 11 are current, 5 lag their newest tag.
// A rule that failed the 2 tag-only repos would fail a correct repository —
// the mechanism that got VERSION-DRIFT rejected one check over.
test('a release listing that could not be fetched is a SKIP, never a pass', () => {
const f = checkReleaseCurrent({ forgeTagsSelf: ['v1.0.0'], releases: null });
assert.equal(f[0].level, 'SKIP');
assert.equal(f[0].skip, 'notRun');
});
test('refs that could not be read are a SKIP too — a missing side is not agreement', () => {
const f = checkReleaseCurrent({ forgeTagsSelf: null, releases: ['v1.0.0'] });
assert.equal(f[0].level, 'SKIP');
assert.equal(f[0].skip, 'notRun');
});
test('no tags on the forge is an OK — the check ran and found no subject', () => {
// The VERSION-NONE / TAGS-NONE shape: nothing could have been released, so
// there is nothing here to be wrong. Measured on 4 of 22 repos.
const f = checkReleaseCurrent({ forgeTagsSelf: [], releases: [] });
assert.equal(f[0].level, 'OK');
assert.equal(f[0].code, 'RELEASE-NONE');
});
test('tags with no releases is an OK — tag-only is a convention, not a defect', () => {
// ktg-plugin-marketplace (9 tags) and llm-security-commons (8) publish no
// releases at all. Nothing in a clone says which convention a repo follows.
const f = checkReleaseCurrent({ forgeTagsSelf: ['v0.1.0', 'v0.4.3'], releases: [] });
assert.equal(f[0].level, 'OK');
assert.equal(f[0].code, 'RELEASE-TAGS-ONLY');
assert.match(f[0].msg, /no release/i);
});
test('the newest release matching the newest tag is current', () => {
const f = checkReleaseCurrent({ forgeTagsSelf: ['v7.8.2', 'v7.8.3'], releases: ['v7.8.3'] });
assert.equal(f[0].level, 'OK');
assert.equal(f[0].code, 'RELEASE-CURRENT');
});
test('a release behind the newest tag is a WARN naming both versions', () => {
// repo-standard itself: v0.9.0 tagged, v0.3.0 published. The remedy — publish
// a release for a tag that already exists — is safe, unlike force-moving a
// published ref, so this is a WARN and not an ERROR.
const f = checkReleaseCurrent({ forgeTagsSelf: ['v0.3.0', 'v0.9.0'], releases: ['v0.2.2', 'v0.3.0'] });
assert.equal(f[0].level, 'WARN');
assert.equal(f[0].code, 'RELEASE-STALE');
assert.equal(f[0].bucket, 'weakening');
assert.match(f[0].msg, /v0\.9\.0/);
assert.match(f[0].msg, /v0\.3\.0/);
});
test('newest is version order, not lexical — v0.10.0 outranks v0.9.0', () => {
// The defect this repo already fixed once in compareTags, and reproduced a
// second time in the shell that MEASURED this check's baseline. okr is the
// live case: tag v1.10.0, release v1.9.0.
const stale = checkReleaseCurrent({ forgeTagsSelf: ['v1.9.0', 'v1.10.0'], releases: ['v1.9.0'] });
assert.equal(stale[0].code, 'RELEASE-STALE');
const current = checkReleaseCurrent({ forgeTagsSelf: ['v1.9.0', 'v1.10.0'], releases: ['v1.10.0'] });
assert.equal(current[0].code, 'RELEASE-CURRENT');
});
test('a pre-release tag published as a release is current', () => {
// llm-ingestion-okf sits on v0.5.0a2 in both places. A repo on a pre-release
// could never reach 0 findings if the suffix were truncated away.
const f = checkReleaseCurrent({ forgeTagsSelf: ['v0.4.0', 'v0.5.0a1', 'v0.5.0a2'], releases: ['v0.5.0a2'] });
assert.equal(f[0].code, 'RELEASE-CURRENT');
});
test('a release ahead of every known tag is not reported as stale', () => {
const f = checkReleaseCurrent({ forgeTagsSelf: ['v1.0.0'], releases: ['v1.1.0'] });
assert.equal(f[0].level, 'OK');
});
// ------------------------------------------------------------- REMOTE-SYNC
//
// Measured 2026-08-12 across all 21 registered clones: exactly one repo has a
// local tag the forge does not (portfolio-optimiser, v1.0.0), and none is
// behind. One subject is what got BRANCH-STALE rejected — the difference is
// that an unpushed tag is never a legitimate convention, and that the gate
// currently says the WRONG thing about it: VERSION-TAG reads LOCAL tags, so a
// manifest claiming 1.0.0 against an unpushed v1.0.0 reads as OK while no
// stranger can resolve it.
test('a local tag the forge does not have is an ERROR naming the tag', () => {
const f = checkRemoteSync({ tags: ['v0.1.0', 'v1.0.0'], forgeTagsSelf: ['v0.1.0'] });
assert.equal(f[0].level, 'ERROR');
assert.equal(f[0].code, 'REMOTE-SYNC');
assert.equal(f[0].bucket, 'broken');
assert.match(f[0].msg, /v1\.0\.0/);
});
test('being behind the forge is NOT a finding — a stale fetch is not a defect', () => {
// The reverse direction must never fire. Every clone that has not fetched
// lately would fail, and nothing is wrong with the repository.
const f = checkRemoteSync({ tags: ['v0.1.0'], forgeTagsSelf: ['v0.1.0', 'v0.2.0'] });
assert.equal(f[0].level, 'OK');
});
test('refs that could not be read are a SKIP, never a pass', () => {
const f = checkRemoteSync({ tags: ['v1.0.0'], forgeTagsSelf: null });
assert.equal(f[0].level, 'SKIP');
assert.equal(f[0].skip, 'notRun');
});
test('no local tags is an OK — there is nothing that could be unpushed', () => {
const f = checkRemoteSync({ tags: [], forgeTagsSelf: [] });
assert.equal(f[0].level, 'OK');
assert.equal(f[0].code, 'REMOTE-SYNC');
});
test('every local tag present on the forge is an OK', () => {
const f = checkRemoteSync({ tags: ['v0.1.0', 'v0.2.0'], forgeTagsSelf: ['v0.1.0', 'v0.2.0'] });
assert.equal(f[0].level, 'OK');
});
test('several unpushed tags are reported in version order, in one finding', () => {
// The remedy is safe for all of them — pushing a tag moves no published ref —
// so unlike TAG-ANNOTATED-HISTORY there is no reason to collapse them to a
// count. Order is the engine's, so v0.10.0 does not sort under v0.9.0.
const f = checkRemoteSync({ tags: ['v0.9.0', 'v0.10.0'], forgeTagsSelf: [] });
assert.equal(f.length, 1);
assert.match(f[0].msg, /v0\.9\.0.*v0\.10\.0/);
});
test('a non-semver forge tag cannot manufacture a stale release', () => {
// `forgeTags` returns EVERY ref, deliberately: 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
// (`config-audit-v4.0.0`, `config-audit/v5.0.0`, `config-audit/v5.1.0`,
// `pre-polyrepo-archive`), so RELEASE-CURRENT sees them too. They parse to
// 0.0.0 and can therefore only ever read as BEHIND a real release — never as
// a newer tag the release is lagging.
const f = checkReleaseCurrent({
forgeTagsSelf: ['v7.7.2', 'config-audit/v5.1.0', 'pre-polyrepo-archive'],
releases: ['v7.7.2'],
});
assert.equal(f[0].code, 'RELEASE-CURRENT');
});
test('unfiltered forge refs cannot manufacture an unpushed-tag ERROR either', () => {
// REMOTE-SYNC compares local `v*` tags against the unfiltered forge list, so
// the extra refs can only ever REMOVE findings. The asymmetry is a coverage
// gap — an unpushed non-`v` tag is invisible — never a false positive.
const f = checkRemoteSync({ tags: ['v7.7.2'], forgeTagsSelf: ['v7.7.2', 'pre-polyrepo-archive'] });
assert.equal(f[0].level, 'OK');
});
// ----------------------------------------------------------- VERIFY-COMMAND
//
// The org publishes no CI badge because it has no CI — this repo's own stated
// substitute is "one command from a clean clone, said plainly". That single
// published stance is what licenses a check firing on a third of the org:
// unlike VERSION-DRIFT, there is no second legitimate convention a repo could
// be following. A repo with a runnable suite and no documented command is not
// on the other convention; it is undocumented.
//
// Measured 2026-08-12 across all 21 registered clones. Subject present (a
// `scripts.test` entry or a tracked executable test file) in 16 of 21; the
// five without one — human-friendly-style, llm-security-commons,
// playground-design-system, portfolio-optimiser-commons, app-creator — hold
// prose, output styles and domain packs, nothing a stranger could run. Those
// five are exactly the repos a class-level requirement would have failed for
// being correct, which is why the subject is MEASURED and not read off a class.
//
// The check can only ever fell a MISSING command. It never runs one, so it can
// never report that a documented command works — the message must not imply it.
test('a repo with nothing runnable is an OK, not a skip — the check found no subject', () => {
// The VERSION-NONE / RELEASE-NONE shape: the check ran, saw everything, and
// there was nothing here to be wrong.
const f = checkVerifyCommand({ readme: '# x\n', testScript: null, testFileCount: 0 });
assert.equal(f[0].level, 'OK');
assert.equal(f[0].code, 'VERIFY-NONE');
});
test('a suite with no command anywhere in the README is a WARN in the missing bucket', () => {
const f = checkVerifyCommand({
readme: '# okr\n\nA plugin.\n',
testScript: 'node --test tests/',
testFileCount: 24,
});
assert.equal(f[0].level, 'WARN');
assert.equal(f[0].code, 'VERIFY-MISSING');
assert.equal(f[0].bucket, 'missing');
});
test('the WARN names the command the repo already has, so the remedy is one line', () => {
const f = checkVerifyCommand({ readme: '# okr\n', testScript: 'node --test tests/', testFileCount: 24 });
assert.match(f[0].msg, /node --test tests\//);
});
test('a suite with no `scripts.test` still gets a WARN, and it names the file count instead', () => {
const f = checkVerifyCommand({ readme: '# config-audit\n', testScript: null, testFileCount: 106 });
assert.equal(f[0].code, 'VERIFY-MISSING');
assert.match(f[0].msg, /106/);
});
test('a fenced `npm test` is an OK naming the command it found', () => {
const readme = '# x\n\n## Tests\n\n```bash\nnpm test\n```\n';
const f = checkVerifyCommand({ readme, testScript: 'node --test', testFileCount: 3 });
assert.equal(f[0].level, 'OK');
assert.equal(f[0].code, 'VERIFY-COMMAND');
assert.match(f[0].msg, /npm test/);
});
test('the OK never claims the command works — this gate does not run it', () => {
// The one thing this check must not oversell. It can fell a missing command
// and nothing else; a green line here means "documented", never "passing".
const readme = '# x\n\n```bash\nnpm test\n```\n';
const f = checkVerifyCommand({ readme, testScript: 'npm test', testFileCount: 1 });
assert.doesNotMatch(f[0].msg, /passe?s|works|green|verified/i);
assert.match(f[0].msg, /not run|does not run/i);
});
test('a command in prose does not count — a stranger copies out of a code block', () => {
const readme = '# x\n\nRun npm test to check it.\n';
const f = checkVerifyCommand({ readme, testScript: 'npm test', testFileCount: 1 });
assert.equal(f[0].code, 'VERIFY-MISSING');
});
test('an indented code block counts as one — Markdown has two fences', () => {
const readme = '# x\n\nTests:\n\n pytest -q\n';
const f = checkVerifyCommand({ readme, testScript: null, testFileCount: 25 });
assert.equal(f[0].code, 'VERIFY-COMMAND');
});
test('`npm install` is not a verification command', () => {
// The install block is fenced in every repo in the org. Matching it would
// hand a green line to every repo the check exists to find.
const readme = '# x\n\n## Install\n\n```bash\nnpm install\n```\n';
const f = checkVerifyCommand({ readme, testScript: 'npm test', testFileCount: 1 });
assert.equal(f[0].code, 'VERIFY-MISSING');
});
test('the families a clean clone actually runs are all recognised', () => {
// Measured from the corpus rather than imagined: npm/pnpm/yarn scripts,
// `node --test`, a named test file, pytest, make, a shell test script, and
// the `--selftest` flag repo-mailbox ships.
const cases = [
'npm test',
'npm run test:unit',
'pnpm test',
'yarn test',
'node --test',
'node scripts/repo-standard-check.test.mjs',
'pytest',
'python3 -m pytest tests/',
'make check',
'./scripts/coord-selftest.sh',
'bash tests/run-tests.sh',
'coord-selftest --selftest',
];
for (const cmd of cases) {
const f = checkVerifyCommand({
readme: `# x\n\n\`\`\`bash\n${cmd}\n\`\`\`\n`,
testScript: null,
testFileCount: 1,
});
assert.equal(f[0].code, 'VERIFY-COMMAND', `not recognised: ${cmd}`);
}
});
test('a `$` prompt prefix does not hide the command', () => {
const f = checkVerifyCommand({ readme: '# x\n\n```\n$ npm test\n```\n', testScript: null, testFileCount: 1 });
assert.equal(f[0].code, 'VERIFY-COMMAND');
});
test('VERIFY-COMMAND reads no network input, so it has no SKIP at all', () => {
// Every check written since PIN-DEAD opens by guarding a null network input.
// This one reads the README and package.json only — copying that reflex would
// print a false "not run" for a check that ran perfectly well offline.
for (const args of [
{ readme: '', testScript: null, testFileCount: 0 },
{ readme: '', testScript: 'npm test', testFileCount: 0 },
{ readme: '```\nnpm test\n```', testScript: null, testFileCount: 9 },
]) {
assert.equal(checkVerifyCommand(args).every((f) => f.level !== 'SKIP'), true);
}
});
test('codeLines returns what stripCode blanked, and nothing else', () => {
// One fence state machine, not two. The link checks strip code out; this one
// needs exactly the complement, and a second hand-rolled parser is how the
// two copies drift.
const md = '# t\n\nprose npm test\n\n```bash\nnpm test\n```\n\n pytest -q\n';
const lines = codeLines(md);
assert.deepEqual(lines.map((l) => l.trim()), ['npm test', 'pytest -q']);
});
test('classifyRepo carries the verification inputs through to a finding', () => {
// The wiring, not the rule: a check that is never called from the pipeline
// passes its own unit tests forever while measuring nothing.
const r = classifyRepo(
{ name: 'repo-mailbox', files: {}, present: [], description: null, testScript: 'npm test', testFileCount: 4 },
REGISTER,
);
assert.equal(r.findings.some((f) => f.code === 'VERIFY-MISSING'), true);
});
test('countTestFiles counts executable tests, not everything under tests/', () => {
// The looser rule counted golden files and transcripts. A WARN saying "you
// have tests a stranger cannot run" is false the moment its subject is a
// fixture — measured on portfolio-optimiser's tests/golden/*.stdout.
const tracked = [
'scripts/x.test.mjs',
'tests/test_engine.py',
'tests/engine_test.py',
'scripts/coord-selftest.sh',
'tests/run-tests.sh',
'tests/golden/demo-transcript.stdout',
'tests/conftest.py',
'tests/fixtures/sample.md',
'README.md',
];
assert.equal(countTestFiles(tracked), 5);
assert.equal(countTestFiles([]), 0);
assert.equal(countTestFiles(null), 0);
});
test('a `test` substring alone does not make a shell script a test', () => {
// `latest-release.sh` contains `test`. The shell family needs a boundary the
// other families get for free from their suffixes.
assert.equal(countTestFiles(['scripts/latest-release.sh', 'tests/conftest.py']), 0);
});
test('a directory separator is a name boundary too, or a whole repo reads as having no tests', () => {
// claude-design ships five shell tests under `tests/` and read as VERIFY-NONE
// — a false "nothing here to verify", which is a claim about the repo, not a
// missing finding. `^` only anchors the whole string; the basename of
// `tests/test-sc1-dogfood-log.sh` starts after a slash.
assert.equal(countTestFiles([
'tests/test-sc1-dogfood-log.sh',
'tests/test-skill-triggers.sh',
'tests/validate-plugin.sh',
]), 3);
});

View file

@ -26,9 +26,11 @@ the script cannot encode.
node "${CLAUDE_PLUGIN_ROOT}/scripts/repo-standard-check.mjs" --dir "$PWD"
Findings are `ERROR` (blocks), `WARN` (look, then decide), `SKIP` (not judged —
in one of two ways, see below), `OK`. Exit 1 on any ERROR. Add `--offline` to skip the two
network calls, `--json` for machine output, `--refresh` to compare the bundled
register against the live org listing.
in one of two ways, see below), `OK`. Exit 1 on any ERROR. Add `--offline` to skip the three
API calls — the org listing, the catalog manifest, and the repo's releases —
`--json` for machine output, `--refresh` to compare the bundled register against
the live org listing. Refs are read over the git protocol instead and cost
nothing against that budget.
**Never report a `SKIP` as a pass.** A SKIP means the gate did not judge the
thing. Say which, and say which KIND — since 0.8.0 there are two, and they are
@ -45,7 +47,8 @@ not interchangeable:
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") and `LANGUAGE` ("no running prose to judge") are `OK` and
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.
@ -156,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
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
Mermaid renders in a README on this forge. That was verified on the live