feat(gate): two presentation checks the evidence actually supports
Adversarial deep research (25 sources, 124 claims extracted, 25 verified: 11 confirmed / 14 refuted) plus direct measurement of all 18 cloned open/ repos. The useful half of the result is what it REFUSED to support, so the research is recorded in docs/presentation-research-2026-08-03.md rather than being spent and forgotten. - BADGE-COUNT (WARN) — past five badges. Trockman et al., ICSE 2018 (n=294,941 npm packages) measured a non-linear relationship with popularity inflecting at five, motivated by surveyed maintainers calling over-badged READMEs cluttered and "trying too hard". WARN and never ERROR: the coefficient sits in an appendix with no CI or p-value. Counting deliberately uses a NARROWER rule than the existing claim check, so a screenshot or an architecture diagram is never counted as clutter. Fires on 8 of 18. - README-LANGUAGE (WARN) — prose not in the language this repo's readers were declared to speak, via a new `locales` axis in the register. Class is structural, a trait is what the code DOES, a locale is who it is FOR — the standard's own "who the reader is decides what is required". English is the default; ms-ai-architect and okr are declared nb, named by the operator as Norway-only in audience. Stopword-frequency comparison over prose with code stripped: a Norwegian flag name in a shell example cannot decide the document. Fires on exactly those two, silent on all sixteen English repos. One design correction found mid-implementation: the first version returned SKIP when a README had too little prose to judge, which broke a passing fixture and would have stopped any terse repo from ever reaching OK. SKIP is for a check that could not RUN; this one ran, saw everything and found no prose to be in the wrong language — the same shape as "no licence claim to back". Insufficient prose is now OK, and evenly bilingual prose is the SKIP, because there the question is live and unanswered. Deliberately NOT built, because the evidence does not reach: any rule about images, diagrams or terminal recordings (every such claim refuted 0-3); a README length bound (no evidence-based target exists); a section count (would fire on 9 of 18 — textbook "suspect the CHECK"); Mermaid source length and #gh-dark-mode-only (zero occurrences, and the instance limit is not readable via the API, so any threshold would be a guess). Verified live against the operator's own forge (15.0.6+gitea-1.22.0): Mermaid DOES render in README.md — two div.mermaid-block iframes carrying real SVG — while #gh-dark-mode-only landed only in Gitea 1.26.0 and is unavailable here. 103 tests green, up from 92. No version bump: the catalog ref still trails at v0.1.1 against 0.1.3, and starting a second release chain over that is the operator's call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TCAGKZT8h9F46ygzSkhEee
This commit is contained in:
parent
5eba10acdd
commit
dc386d4471
7 changed files with 474 additions and 2 deletions
|
|
@ -434,14 +434,30 @@ export function checkVersionConsistency({ pluginVersion, readmeBadge, changelogT
|
|||
// word loses no real detection.
|
||||
const CLAIM_BADGE = /(tests?|build|ci|coverage|passing)/i;
|
||||
|
||||
// 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
|
||||
// matters: counting a screenshot or an architecture diagram as clutter would
|
||||
// punish exactly the visual work this standard wants more of.
|
||||
const BADGE_URL = /shields\.io|badgen\.net|\/badges?[/.]/i;
|
||||
|
||||
// Trockman et al., ICSE 2018 (doi 10.1145/3180155.3180209, n=294,941 npm
|
||||
// packages): badge count relates to popularity non-linearly with a predicted
|
||||
// inflection at five, and surveyed maintainers called over-badged READMEs
|
||||
// cluttered and "trying too hard". WARN, never ERROR — the coefficient sits in
|
||||
// an appendix with no CI or p-value, so it carries "more is not better" and
|
||||
// cannot carry a hard limit.
|
||||
const BADGE_INFLECTION = 5;
|
||||
|
||||
export function checkBadges({ readme }) {
|
||||
const findings = [];
|
||||
let badgeCount = 0;
|
||||
for (const line of String(readme ?? '').split('\n')) {
|
||||
// Any image, any host. Restricting this to img.shields.io would have missed
|
||||
// a self-hosted SVG asserting exactly the same unverified thing.
|
||||
for (const m of line.matchAll(/(\[)?!\[([^\]]*)\]\(([^)\s]+)\)(\])?/g)) {
|
||||
const linked = m[1] === '[' && m[4] === ']';
|
||||
const label = `${m[2]} ${m[3]}`;
|
||||
if (BADGE_URL.test(m[3])) badgeCount++;
|
||||
if (!linked && CLAIM_BADGE.test(label)) {
|
||||
findings.push({
|
||||
level: 'WARN',
|
||||
|
|
@ -452,10 +468,92 @@ export function checkBadges({ readme }) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (badgeCount > BADGE_INFLECTION) {
|
||||
findings.push({
|
||||
level: 'WARN',
|
||||
code: 'BADGE-COUNT',
|
||||
bucket: 'weakening',
|
||||
msg: `${badgeCount} badges — past the measured inflection of ${BADGE_INFLECTION}, where a badge row starts reading as clutter rather than as evidence (Trockman et al., ICSE 2018). Keep the ones a reader acts on.`,
|
||||
});
|
||||
}
|
||||
if (findings.length === 0) findings.push({ level: 'OK', code: 'BADGES', msg: 'no static badge asserts an unverified run' });
|
||||
return findings;
|
||||
}
|
||||
|
||||
// Which language a README is written in is not a property of the code, so no
|
||||
// remote can report it — it is a property of the READER, and the operator owns
|
||||
// it. English is the default; a repo aimed only at a Norwegian readership is
|
||||
// declared `nb` in the register and is then wrong in English, not right.
|
||||
//
|
||||
// Detection is a stopword-frequency comparison rather than a dependency: both
|
||||
// word sets below are chosen to have NO member that is also a common word in
|
||||
// the other language, which is why `at` and `for` (Norwegian and English both)
|
||||
// are deliberately absent from each.
|
||||
const STOPWORDS = {
|
||||
nb: ['og', 'ikke', 'som', 'det', 'den', 'er', 'på', 'til', 'av', 'med', 'om',
|
||||
'har', 'kan', 'skal', 'blir', 'være', 'etter', 'når', 'også', 'hvis',
|
||||
'eller', 'men', 'fra', 'ved', 'mot', 'uten', 'hver', 'alle', 'andre',
|
||||
'seg', 'dette', 'disse', 'mellom', 'gjennom', 'siden', 'fordi', 'derfor'],
|
||||
en: ['the', 'and', 'of', 'to', 'in', 'is', 'that', 'with', 'this', 'are',
|
||||
'be', 'from', 'by', 'as', 'an', 'or', 'not', 'you', 'your', 'we', 'our',
|
||||
'it', 'on', 'which', 'when', 'what', 'how', 'its', 'they', 'their', 'has',
|
||||
'can', 'will', 'should', 'each', 'between', 'because', 'therefore'],
|
||||
};
|
||||
|
||||
// Below this there is not enough running prose for a frequency count to mean
|
||||
// anything, and below the ratio the document is genuinely mixed. Both say so
|
||||
// rather than guessing — a wrong verdict on language is worse than no verdict.
|
||||
const LANG_MIN_HITS = 10;
|
||||
const LANG_MIN_RATIO = 1.5;
|
||||
|
||||
function countStopwords(text, words) {
|
||||
const lower = text.toLowerCase();
|
||||
let n = 0;
|
||||
for (const w of words) {
|
||||
const m = lower.match(new RegExp(`(^|[^\\p{L}])${w}([^\\p{L}]|$)`, 'gu'));
|
||||
if (m) n += m.length;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
export function checkReadmeLanguage({ readme, name }, register) {
|
||||
const declared = register?.locales?.[name] ?? 'en';
|
||||
const other = declared === 'nb' ? 'en' : 'nb';
|
||||
// Same discipline as the link and boilerplate checks: a Norwegian flag name
|
||||
// in a shell example must not decide what language the DOCUMENT is in.
|
||||
const prose = stripCode(String(readme ?? ''));
|
||||
const hits = { nb: countStopwords(prose, STOPWORDS.nb), en: countStopwords(prose, STOPWORDS.en) };
|
||||
|
||||
// Not a SKIP. SKIP is for a check that could not RUN — the catalog was
|
||||
// unreachable, the file unreadable. This one ran, saw everything, and found
|
||||
// no prose to be in the wrong language, the same shape as "no licence claim
|
||||
// to back". A thin README is a real problem, and it is checkFirstScreen's;
|
||||
// routing it here would stop any terse repo from ever reaching OK.
|
||||
if (hits[declared] + hits[other] < LANG_MIN_HITS) {
|
||||
return [{
|
||||
level: 'OK',
|
||||
code: 'LANGUAGE',
|
||||
msg: `no running prose to judge (${hits[declared] + hits[other]} marker words) — nothing claims a language`,
|
||||
}];
|
||||
}
|
||||
if (hits[other] >= hits[declared] * LANG_MIN_RATIO) {
|
||||
return [{
|
||||
level: 'WARN',
|
||||
code: 'README-LANGUAGE',
|
||||
bucket: 'weakening',
|
||||
msg: `README reads as \`${other}\` but this repo is declared \`${declared}\` (${hits[other]} vs ${hits[declared]} marker words). Who the reader is decides the language — fix the prose, or fix \`locales\` in the register.`,
|
||||
}];
|
||||
}
|
||||
if (hits[declared] < hits[other] * LANG_MIN_RATIO) {
|
||||
return [{
|
||||
level: 'SKIP',
|
||||
code: 'README-LANGUAGE-UNDECIDABLE',
|
||||
msg: `README mixes languages too evenly to call (${hits.nb} nb vs ${hits.en} en) — declared \`${declared}\`, unverified`,
|
||||
}];
|
||||
}
|
||||
return [{ level: 'OK', code: 'LANGUAGE', msg: `README reads as \`${declared}\`, as declared` }];
|
||||
}
|
||||
|
||||
// Template text that was never filled in. A visible unfinished template costs
|
||||
// more trust than the missing document would have.
|
||||
const FIXME_RE = /FIXME/;
|
||||
|
|
@ -689,6 +787,7 @@ export function classifyRepo(
|
|||
...checkInternalLinks({ files, present }),
|
||||
...checkLicenseClaim({ readme, present }),
|
||||
...checkBadges({ readme }),
|
||||
...checkReadmeLanguage({ readme, name }, register),
|
||||
...checkBoilerplate({ files }),
|
||||
...checkVersionConsistency({ pluginVersion, readmeBadge, changelogTop, tags }),
|
||||
...checkDescription(description, register),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue