Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45a2bf30de | |||
| e061c1d219 | |||
| 1ee6cc28c6 | |||
| 5fe1743105 | |||
| aee0f28539 | |||
| 4714f13959 | |||
| 1a72f8dde5 | |||
| 1b0a9a338e | |||
| 198c95e5e5 |
7 changed files with 313 additions and 21 deletions
15
CLAUDE.md
15
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
|
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 —
|
looked" the same output. The wanted side effect is exposure, not silence —
|
||||||
`ai-psychosis` is deliberately unregistered so it stands alone.
|
`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`
|
- **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`
|
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
|
naming why. An exception nobody can see reads exactly like a check that
|
||||||
|
|
|
||||||
31
README.md
31
README.md
|
|
@ -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
|
command, from a clean clone: `npm test`. A badge asserting it would be a claim
|
||||||
dressed as evidence.
|
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
|
## Install
|
||||||
|
|
||||||
Use the `https://` form. The forge UI's clone button hands out an `ssh://` URL,
|
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 block | the form for this class is missing, incomplete, shown over `ssh://`, or points at the wrong marketplace |
|
||||||
| Install truth | the plugin is not pinned in the catalog, so the documented command cannot succeed for anyone |
|
| Install truth | the plugin is not pinned in the catalog, so the documented command cannot succeed for anyone |
|
||||||
| Install pins | a `@v…` or `--branch v…` in an install command names a tag the forge does not have, so the command a stranger copies 404s |
|
| 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 |
|
| Required files | a file this class (or trait) needs is absent |
|
||||||
| Repo references | an `open/<name>` in URL position resolves to nothing |
|
| Repo references | an `open/<name>` in URL position resolves to nothing |
|
||||||
| Relative links | a link points at a file that is not tracked |
|
| Relative links | a link points at a file that is not tracked |
|
||||||
|
|
@ -180,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
|
warning, and the message names both — that is drift in one of the two, not a
|
||||||
title.
|
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
|
### When equality is the wrong demand
|
||||||
|
|
||||||
`README-DESC` requires the README's opening line to be the forge description
|
`README-DESC` requires the README's opening line to be the forge description
|
||||||
|
|
|
||||||
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",
|
"name": "repo-standard",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
"playground-design-system": "shared-asset",
|
"playground-design-system": "shared-asset",
|
||||||
"portfolio-optimiser-commons": "shared-asset",
|
"portfolio-optimiser-commons": "shared-asset",
|
||||||
"llm-security-commons": "shared-asset",
|
"llm-security-commons": "shared-asset",
|
||||||
|
"ki-produktivitetsmodell": "shared-asset",
|
||||||
".profile": "org-profile",
|
".profile": "org-profile",
|
||||||
"portfolio-optimiser": "standalone",
|
"portfolio-optimiser": "standalone",
|
||||||
"portfolio-optimiser-claude": "standalone",
|
"portfolio-optimiser-claude": "standalone",
|
||||||
|
|
@ -134,14 +135,27 @@
|
||||||
"",
|
"",
|
||||||
"`ai-psychosis` (`# Interaction Awareness`) is DELIBERATELY ABSENT: it is the",
|
"`ai-psychosis` (`# Interaction Awareness`) is DELIBERATELY ABSENT: it is the",
|
||||||
"one where a reader cannot connect the title to the repo. Leaving it standing",
|
"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": {
|
"titles": {
|
||||||
".profile": "fromaitochitta / open",
|
".profile": "From AI to Chitta — open",
|
||||||
"llm-ingestion-pipeline-security": "llm-ingestion-guard",
|
"llm-ingestion-pipeline-security": "llm-ingestion-guard",
|
||||||
"linkedin-studio": "LinkedIn Studio Plugin for Claude Code",
|
"linkedin-studio": "LinkedIn Studio Plugin for Claude Code",
|
||||||
"llm-security": "LLM Security 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": [
|
"$comment_traits": [
|
||||||
|
|
@ -186,11 +200,45 @@
|
||||||
"about two paragraphs up. The register was the wrong side, not the prose.",
|
"about two paragraphs up. The register was the wrong side, not the prose.",
|
||||||
"`okr` stands: its README IS Norwegian, rewritten to close census 05.",
|
"`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.",
|
"Detection is a stopword-frequency comparison over prose with code stripped.",
|
||||||
"It answers WHICH language dominates, never whether the prose is any good."
|
"It answers WHICH language dominates, never whether the prose is any good."
|
||||||
],
|
],
|
||||||
"locales": {
|
"locales": {
|
||||||
"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": [
|
"$comment_tags_lightweight_accepted": [
|
||||||
|
|
@ -222,6 +270,15 @@
|
||||||
"itself enforces that, since a new lightweight newest tag is not on this",
|
"itself enforces that, since a new lightweight newest tag is not on this",
|
||||||
"list and fires ERROR.",
|
"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",
|
"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",
|
"by construction, not omitted by a fetch gap: the engine reads",
|
||||||
"`refs/tags/v*` — the repo's own version line — and a namespaced",
|
"`refs/tags/v*` — the repo's own version line — and a namespaced",
|
||||||
|
|
|
||||||
|
|
@ -575,16 +575,32 @@ export function checkRequiredFiles({ present, klass, traits }, register) {
|
||||||
// on a predictable heading is what agents pattern-match on, and `## Non-goals`
|
// 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
|
// is the cheapest trust-builder there is: it proves someone thought about the
|
||||||
// boundary, and it stops misuse before it starts.
|
// 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 { headings: required } = requirementsFor(klass, traits, register);
|
||||||
const text = String(readme ?? '');
|
const text = String(readme ?? '');
|
||||||
const present = new Set(
|
const present = new Set(
|
||||||
text.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('#')),
|
text.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('#')),
|
||||||
);
|
);
|
||||||
|
const aliases = register.heading_aliases?.[name] ?? {};
|
||||||
const findings = [];
|
const findings = [];
|
||||||
for (const h of required) {
|
for (const h of required) {
|
||||||
if ([...present].some((p) => p.toLowerCase() === h.toLowerCase())) continue;
|
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
|
// Same title, wrong depth: say that, rather than "missing". The contract
|
||||||
// wants a predictable top-level heading because that is what an agent
|
// 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
|
// pattern-matches on — but the section does exist, and the fix is a
|
||||||
|
|
@ -708,7 +724,19 @@ export function checkTagIntegrity({ tagObjects, name }, register) {
|
||||||
|
|
||||||
const findings = [];
|
const findings = [];
|
||||||
const newest = tags[tags.length - 1];
|
const newest = tags[tags.length - 1];
|
||||||
|
const accepted = new Set(register?.tags_lightweight_accepted?.[name] ?? []);
|
||||||
if (!newest.annotated) {
|
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({
|
findings.push({
|
||||||
level: 'ERROR',
|
level: 'ERROR',
|
||||||
code: 'TAG-ANNOTATED',
|
code: 'TAG-ANNOTATED',
|
||||||
|
|
@ -716,7 +744,7 @@ export function checkTagIntegrity({ tagObjects, name }, register) {
|
||||||
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}^{}\`.`,
|
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] ?? []);
|
}
|
||||||
const olderLightweight = tags.slice(0, -1).filter((t) => !t.annotated);
|
const olderLightweight = tags.slice(0, -1).filter((t) => !t.annotated);
|
||||||
const older = olderLightweight.filter((t) => !accepted.has(t.name));
|
const older = olderLightweight.filter((t) => !accepted.has(t.name));
|
||||||
const excused = olderLightweight.filter((t) => accepted.has(t.name));
|
const excused = olderLightweight.filter((t) => accepted.has(t.name));
|
||||||
|
|
@ -842,7 +870,7 @@ export function checkTagSigned({ tagObjects }, register) {
|
||||||
level: 'SKIP',
|
level: 'SKIP',
|
||||||
skip: 'byDesign',
|
skip: 'byDesign',
|
||||||
code: 'TAG-SIGNED-LIGHTWEIGHT',
|
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 that finding; this check declines rather than report one defect twice.`,
|
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.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1356,6 +1384,20 @@ function isFixturePath(path) {
|
||||||
// A home directory is what makes a `file:` URL a leak rather than a scheme the
|
// 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
|
// 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.
|
// 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;
|
const FILE_URL_LEAK = /^file:\/\/\/?(Users|home)\//i;
|
||||||
|
|
||||||
// Relative file links only. Anchor resolution depends on per-renderer heading
|
// Relative file links only. Anchor resolution depends on per-renderer heading
|
||||||
|
|
@ -1530,7 +1572,7 @@ export function classifyRepo(
|
||||||
...checkInstallBlock({ readme, name, klass }, register),
|
...checkInstallBlock({ readme, name, klass }, register),
|
||||||
...checkInstallTruth({ name, klass, catalogNames }),
|
...checkInstallTruth({ name, klass, catalogNames }),
|
||||||
...checkInstallPins({ readme, forgeTagsByRepo }, register),
|
...checkInstallPins({ readme, forgeTagsByRepo }, register),
|
||||||
...checkHeadings({ readme, klass, traits }, register),
|
...checkHeadings({ readme, klass, traits, name }, register),
|
||||||
...checkRequiredFiles({ present, klass, traits }, register),
|
...checkRequiredFiles({ present, klass, traits }, register),
|
||||||
...checkLinks({ files }, register),
|
...checkLinks({ files }, register),
|
||||||
...checkInternalLinks({ files, present }),
|
...checkInternalLinks({ files, present }),
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@
|
||||||
// They are the reason this gate has three outcomes instead of a boolean.
|
// They are the reason this gate has three outcomes instead of a boolean.
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
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 {
|
import {
|
||||||
countCodepoints,
|
countCodepoints,
|
||||||
normalizeRepoRef,
|
normalizeRepoRef,
|
||||||
|
|
@ -47,6 +50,7 @@ import {
|
||||||
checkVerifyCommand,
|
checkVerifyCommand,
|
||||||
countTestFiles,
|
countTestFiles,
|
||||||
codeLines,
|
codeLines,
|
||||||
|
inspectRepo,
|
||||||
} from './repo-standard-check.mjs';
|
} from './repo-standard-check.mjs';
|
||||||
|
|
||||||
const REGISTER = {
|
const REGISTER = {
|
||||||
|
|
@ -766,6 +770,27 @@ test('org-profile requires no headings at all', () => {
|
||||||
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
|
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
|
// ------------------------------------------------------------- file: URL links
|
||||||
|
|
||||||
// A `file:///Users/ktg/...` link is dead for every reader but its author, and
|
// A `file:///Users/ktg/...` link is dead for every reader but its author, and
|
||||||
|
|
@ -811,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);
|
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
|
// -------------------------------------------------------------- tag integrity
|
||||||
|
|
||||||
// A lightweight tag is a branch-like ref: it can be moved to a different commit
|
// A lightweight tag is a branch-like ref: it can be moved to a different commit
|
||||||
|
|
@ -903,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
|
// 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
|
// re-cut at no cost, so acceptance is never inherited from a repo merely
|
||||||
// is the one that cannot be accepted away.
|
// having OTHER accepted entries — only an exact name match on THIS tag excuses
|
||||||
test('the NEWEST lightweight tag is still an ERROR even when the register accepts its name', () => {
|
// it (the exception directly below).
|
||||||
const reg = { tags_lightweight_accepted: { alpha: ['v2.0.0'] } };
|
test('an unaccepted newest lightweight tag is still an ERROR, even when the register has other entries for this repo', () => {
|
||||||
const f = checkTagIntegrity({ tagObjects: [{ name: 'v1.0.0', annotated: true }, { name: 'v2.0.0', annotated: false }] }, reg);
|
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');
|
const hit = f.find((x) => x.code === 'TAG-ANNOTATED');
|
||||||
assert.equal(hit.level, 'ERROR');
|
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', () => {
|
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 tagObjects = [{ name: 'v0.1.0', annotated: false }, { name: 'v1.0.0', annotated: true }];
|
||||||
const f = checkTagIntegrity({ tagObjects, name: 'beta' }, ACCEPT_REG);
|
const f = checkTagIntegrity({ tagObjects, name: 'beta' }, ACCEPT_REG);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue