Compare commits

..

No commits in common. "main" and "v0.11.2" have entirely different histories.

7 changed files with 21 additions and 313 deletions

View file

@ -225,21 +225,6 @@ 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

View file

@ -21,15 +21,6 @@ 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,
@ -78,7 +69,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. 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 headings | `## Install`, `## Non-goals`, `## Changelog` — per class. Present at the wrong depth is its own finding |
| 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 |
@ -189,26 +180,6 @@ 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

View file

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

View file

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

View file

@ -32,7 +32,6 @@
"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",
@ -135,27 +134,14 @@
"",
"`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.",
"",
"`.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."
"alone is the wanted effect of registering the others."
],
"titles": {
".profile": "From AI to Chitta — open",
".profile": "fromaitochitta / 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",
"ki-produktivitetsmodell": "Tre nivå av organisatorisk produktivitet med KI"
"ms-ai-architect": "AI Architect Plugin for Claude Code"
},
"$comment_traits": [
@ -200,45 +186,11 @@
"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": {
"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"
}
"okr": "nb"
},
"$comment_tags_lightweight_accepted": [
@ -270,15 +222,6 @@
"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",

View file

@ -575,32 +575,16 @@ 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, name }, register) {
export function checkHeadings({ readme, klass, traits }, 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
@ -724,27 +708,15 @@ export function checkTagIntegrity({ tagObjects, name }, register) {
const findings = [];
const newest = tags[tags.length - 1];
const accepted = new Set(register?.tags_lightweight_accepted?.[name] ?? []);
if (!newest.annotated) {
if (accepted.has(newest.name)) {
// Named acceptance, not a standing exemption: only THIS exact tag is
// excused, so a later real release still fires ERROR the moment it
// becomes newest and isn't itself on the list (proven by the sibling
// test below).
findings.push({
level: 'OK',
code: 'TAG-ANNOTATED-ACCEPTED-NEWEST',
msg: `newest tag \`${newest.name}\` is lightweight, but the register accepts it by name as an exception to the newest-tag rule — a safe remedy exists (\`git tag -a -f\`) but costs more than the finding for this specific tag. Any OTHER tag that becomes newest is still judged.`,
});
} else {
findings.push({
level: 'ERROR',
code: 'TAG-ANNOTATED',
bucket: 'broken',
msg: `newest tag \`${newest.name}\` is lightweight — it can be moved to another commit with no record that it ever pointed elsewhere, and the catalog pins releases by tag. Re-cut it annotated: \`git tag -a -f ${newest.name} ${newest.name}^{}\`.`,
});
}
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] ?? []);
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));
@ -870,7 +842,7 @@ export function checkTagSigned({ tagObjects }, register) {
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.`,
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.`,
});
}
@ -1384,20 +1356,6 @@ 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
@ -1572,7 +1530,7 @@ export function classifyRepo(
...checkInstallBlock({ readme, name, klass }, register),
...checkInstallTruth({ name, klass, catalogNames }),
...checkInstallPins({ readme, forgeTagsByRepo }, register),
...checkHeadings({ readme, klass, traits, name }, register),
...checkHeadings({ readme, klass, traits }, register),
...checkRequiredFiles({ present, klass, traits }, register),
...checkLinks({ files }, register),
...checkInternalLinks({ files, present }),

View file

@ -7,10 +7,7 @@
// 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, mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { readFileSync } from 'node:fs';
import {
countCodepoints,
normalizeRepoRef,
@ -50,7 +47,6 @@ import {
checkVerifyCommand,
countTestFiles,
codeLines,
inspectRepo,
} from './repo-standard-check.mjs';
const REGISTER = {
@ -770,27 +766,6 @@ 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
@ -836,57 +811,6 @@ 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
@ -979,54 +903,15 @@ 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, 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);
// 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);
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);