Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45a2bf30de | |||
| e061c1d219 | |||
| 1ee6cc28c6 | |||
| 5fe1743105 | |||
| aee0f28539 | |||
| 4714f13959 | |||
| 1a72f8dde5 | |||
| 1b0a9a338e | |||
| 198c95e5e5 | |||
| f08fcc9b80 | |||
| e5cd1bfaaf | |||
| f422b63bb3 | |||
| e6cd8983ca | |||
| 9f08ebc5ed | |||
| 009c911f9e |
10 changed files with 911 additions and 56 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-standard",
|
||||
"version": "0.10.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"
|
||||
|
|
|
|||
123
CHANGELOG.md
123
CHANGELOG.md
|
|
@ -6,6 +6,129 @@ 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
|
||||
(04–07) 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
|
||||
|
|
|
|||
63
CLAUDE.md
63
CLAUDE.md
|
|
@ -225,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
|
||||
|
|
@ -282,16 +297,42 @@ would recreate, in data, exactly the drift this plugin exists to remove.
|
|||
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.
|
||||
- **`TAG-SIGNED` is BLOCKED ON AN OPERATOR DECISION, not rejected — and filing it
|
||||
with the rejections is the error to avoid.** 0 of 18 repos sign their tags
|
||||
today, which looks like the same measurement that killed VERSION-DRIFT and is
|
||||
not: if the operator decides this org signs tags, the check acquires its whole
|
||||
subject the same day. A rejection is permanent; this is one yes/no away.
|
||||
Recorded here so the next session reads a pending question instead of
|
||||
re-measuring a settled one. The scope is also fixed in advance: `git cat-file
|
||||
tag` reads the signature block from the clone, so the check can answer *is it
|
||||
signed* and must never claim *the forge vouches for it* — the forge's
|
||||
"Verified" badge needs a key registered there.
|
||||
- **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
|
||||
|
|
@ -318,7 +359,7 @@ would recreate, in data, exactly the drift this plugin exists to remove.
|
|||
## Commands
|
||||
|
||||
```bash
|
||||
npm test # 230 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
|
||||
|
|
|
|||
40
README.md
40
README.md
|
|
@ -10,7 +10,7 @@ checks that surface in one repository and reports what it finds.
|
|||
|
||||
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
|
@ -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,
|
||||
|
|
@ -69,7 +78,7 @@ The repository's **class** decides what each check means:
|
|||
| Install block | the form for this class is missing, incomplete, shown over `ssh://`, or points at the wrong marketplace |
|
||||
| Install truth | the plugin is not pinned in the catalog, so the documented command cannot succeed for anyone |
|
||||
| Install pins | a `@v…` or `--branch v…` in an install command names a tag the forge does not have, so the command a stranger copies 404s |
|
||||
| Required headings | `## Install`, `## Non-goals`, `## Changelog` — per class. Present at the wrong depth is its own finding |
|
||||
| Required headings | `## Install`, `## Non-goals`, `## Changelog` — per class. Present at the wrong depth is its own finding. 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 |
|
||||
|
|
@ -80,6 +89,7 @@ The repository's **class** decides what each check means:
|
|||
| 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 |
|
||||
|
|
@ -97,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:
|
||||
|
|
@ -179,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
|
||||
|
|
@ -296,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
34
SECURITY.md
Normal 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.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-standard",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"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",
|
||||
|
|
@ -134,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": [
|
||||
|
|
@ -175,15 +189,56 @@
|
|||
"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": [
|
||||
|
|
@ -201,19 +256,65 @@
|
|||
"the very gap REMOTE-SYNC exists to name, so these are claims about three",
|
||||
"OTHER repos and were sent to them by coord so a wrong name can be disputed.",
|
||||
"",
|
||||
"13 entries, not the 14 lightweight tags that exist. `ktg-plugin-marketplace",
|
||||
"v7.7.2` is DELIBERATELY ABSENT: it is that repo's newest tag, the one",
|
||||
"lightweight tag with a safe remedy (`git tag -a -f`), and it is an ERROR",
|
||||
"today. Pre-accepting it would mean that cutting v7.8.0 instead of fixing it",
|
||||
"makes the finding vanish silently — the cheap fix skipped and unrecorded.",
|
||||
"If it ages into history un-remedied, a fresh WARN is the correct signal."
|
||||
"`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"],
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -538,12 +538,19 @@ export function checkInstallPins({ readme, forgeTagsByRepo }, register) {
|
|||
// 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 };
|
||||
|
|
@ -553,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) {
|
||||
|
|
@ -568,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
|
||||
|
|
@ -701,15 +724,27 @@ export function checkTagIntegrity({ tagObjects, name }, register) {
|
|||
|
||||
const findings = [];
|
||||
const newest = tags[tags.length - 1];
|
||||
if (!newest.annotated) {
|
||||
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 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));
|
||||
|
|
@ -737,6 +772,122 @@ export function checkTagIntegrity({ tagObjects, name }, register) {
|
|||
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
|
||||
|
|
@ -1233,6 +1384,20 @@ function isFixturePath(path) {
|
|||
// 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
|
||||
|
|
@ -1407,7 +1572,7 @@ export function classifyRepo(
|
|||
...checkInstallBlock({ readme, name, klass }, register),
|
||||
...checkInstallTruth({ name, klass, catalogNames }),
|
||||
...checkInstallPins({ readme, forgeTagsByRepo }, register),
|
||||
...checkHeadings({ readme, klass, traits }, register),
|
||||
...checkHeadings({ readme, klass, traits, name }, register),
|
||||
...checkRequiredFiles({ present, klass, traits }, register),
|
||||
...checkLinks({ files }, register),
|
||||
...checkInternalLinks({ files, present }),
|
||||
|
|
@ -1417,6 +1582,7 @@ export function classifyRepo(
|
|||
...checkBoilerplate({ files }),
|
||||
...checkVersionConsistency({ pluginVersion, readmeBadge, changelogTop, tags }),
|
||||
...checkTagIntegrity({ tagObjects, name }, register),
|
||||
...checkTagSigned({ tagObjects }, register),
|
||||
...checkReleaseCurrent({ forgeTagsSelf, releases }),
|
||||
...checkRemoteSync({ tags, forgeTagsSelf }),
|
||||
...checkVerifyCommand({ readme, testScript, testFileCount }),
|
||||
|
|
@ -1678,13 +1844,21 @@ function forgeTags(register, repo) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return execFileSync('git', ['-C', dir, 'for-each-ref', '--format=%(objecttype) %(refname:short)', 'refs/tags/v*'], { encoding: 'utf8' })
|
||||
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, ...rest] = line.split(' ');
|
||||
return { name: rest.join(' '), annotated: type === 'tag' };
|
||||
const [type, sig, date, ...rest] = line.split(' ');
|
||||
return { name: rest.join(' '), annotated: type === 'tag', signed: sig === 'signed', date };
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -22,6 +25,7 @@ import {
|
|||
checkHeadings,
|
||||
checkBadges,
|
||||
checkTagIntegrity,
|
||||
checkTagSigned,
|
||||
checkInstallPins,
|
||||
extractInstallPins,
|
||||
checkReadmeLanguage,
|
||||
|
|
@ -46,6 +50,7 @@ import {
|
|||
checkVerifyCommand,
|
||||
countTestFiles,
|
||||
codeLines,
|
||||
inspectRepo,
|
||||
} from './repo-standard-check.mjs';
|
||||
|
||||
const REGISTER = {
|
||||
|
|
@ -765,6 +770,27 @@ 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
|
||||
|
|
@ -810,6 +836,57 @@ test('other schemes stay somebody else\'s to resolve', () => {
|
|||
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
|
||||
|
|
@ -902,15 +979,54 @@ test('an unaccepted lightweight tag still fires, and the WARN counts only the un
|
|||
});
|
||||
|
||||
// The newest tag is what a consumer resolves today and what an operator can
|
||||
// re-cut at no cost. It is the one lightweight tag with a safe remedy, so it
|
||||
// is the one that cannot be accepted away.
|
||||
test('the NEWEST lightweight tag is still an ERROR even when the register accepts its name', () => {
|
||||
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 }] }, reg);
|
||||
// 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);
|
||||
|
|
@ -934,6 +1050,177 @@ test('newest is the highest version, not the last element of an unsorted list',
|
|||
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
|
||||
|
|
@ -1308,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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue