feat(engine): TAG-ANNOTATED — a movable tag is a movable pin

First of the approved §5 checks. A lightweight tag is a branch-like ref:
it can be moved to another commit with nothing recorded that it ever
pointed elsewhere. The catalog pins every plugin to `ref: vX.Y.Z`, so
this is a supply-chain property, not tidiness.

The two levels come from a measurement, not from taste. Across all 19
clones: 155 tags, 14 lightweight, but only ONE repo whose NEWEST tag is
lightweight. The newest is what a consumer resolves today and what an
operator can re-cut at no cost -> ERROR. The older ones can only be
"fixed" by force-moving an already published ref, which is the exact risk
the check exists to name -> exposed once as a count, WARN, never as
fourteen findings. A gate that demands an unsafe remedy gets switched off.

No tags at all is the VERSION-NONE shape: the check ran, saw every tag
there is, and found no subject. TAGS-NONE is an OK, not a skip.

Newest is decided by version order, not by the order git returns.
`git tag --list` sorts lexically, where v10.0.0 lands before v9.0.0 —
which would misjudge exactly the repos with the longest history
(repo-mailbox has 27 tags). Pinned in test.

Read from local git objects via `for-each-ref %(objecttype)` — zero
network, so the two-call budget is untouched.

Measured on the corpus, and it matches the census exactly: 1 ERROR
(ktg-plugin-marketplace v7.7.2), 3 WARN (catalog 7, okf 5, guard 1),
15 OK, 2 TAGS-NONE. No other repo moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lb7XmJGLnFSX9U7tgS7fKk
This commit is contained in:
Kjell Tore Guttormsen 2026-08-12 21:24:09 +02:00
commit ddfc628761
3 changed files with 164 additions and 2 deletions

View file

@ -21,6 +21,7 @@ import {
checkVersionConsistency,
checkHeadings,
checkBadges,
checkTagIntegrity,
checkReadmeLanguage,
checkBoilerplate,
checkLicenseClaim,
@ -757,6 +758,67 @@ test('org-profile requires no headings at all', () => {
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
// -------------------------------------------------------------- tag integrity
// A lightweight tag is a branch-like ref: it can be moved to a different commit
// with no record that it ever pointed elsewhere. The catalog pins every plugin
// to `ref: vX.Y.Z`, so a movable tag is a movable pin.
//
// Measured across all 19 clones before writing the rule: 155 tags, of which 14
// are lightweight — 8 in catalog, 5 in okf, 1 in guard. Exactly ONE repo has a
// lightweight NEWEST tag. That split is the rule: the newest tag is what a
// consumer resolves today and what an operator can re-cut, so it is an ERROR;
// the older ones can only be "fixed" by force-moving a published ref, which is
// the very risk this check exists to name, so they are exposed as one WARN and
// never as fourteen.
test('a lightweight newest tag is an ERROR — a movable tag is a movable pin', () => {
const f = checkTagIntegrity({ tagObjects: [{ name: 'v1.0.0', annotated: true }, { name: 'v1.1.0', annotated: false }] });
const hit = f.find((x) => x.code === 'TAG-ANNOTATED');
assert.equal(hit.level, 'ERROR');
assert.equal(hit.bucket, 'broken');
assert.match(hit.msg, /v1\.1\.0/);
});
test('all-annotated tags are OK', () => {
const f = checkTagIntegrity({ tagObjects: [{ name: 'v1.0.0', annotated: true }, { name: 'v1.1.0', annotated: true }] });
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
assert.equal(f.some((x) => x.code === 'TAGS'), true);
});
test('older lightweight tags are ONE aggregated WARN, never one finding per tag', () => {
const tagObjects = [
{ name: 'v0.1.0', annotated: false },
{ name: 'v0.2.0', annotated: false },
{ name: 'v0.3.0', annotated: false },
{ name: 'v1.0.0', annotated: true },
];
const f = checkTagIntegrity({ tagObjects });
const warns = f.filter((x) => x.code === 'TAG-ANNOTATED-HISTORY');
assert.equal(warns.length, 1);
assert.equal(warns[0].level, 'WARN');
assert.match(warns[0].msg, /3/);
assert.equal(f.some((x) => x.level === 'ERROR'), false);
});
// Same shape as VERSION-NONE: the check ran, saw every tag there is, and found
// no subject. Nothing here can be wrong, so it is a verdict — not a skip.
test('a repo with no tags has nothing to judge — OK, not SKIP', () => {
const f = checkTagIntegrity({ tagObjects: [] });
assert.equal(f.length, 1);
assert.equal(f[0].level, 'OK');
assert.equal(f[0].code, 'TAGS-NONE');
});
// The newest tag is decided by version order, not by the order git happened to
// hand them over. `git tag --list` sorts lexically, where v10.0.0 sorts BEFORE
// v9.0.0 — reading "newest" off an unsorted list would judge the wrong tag on
// exactly the repos with the longest release history.
test('newest is the highest version, not the last element of an unsorted list', () => {
const f = checkTagIntegrity({ tagObjects: [{ name: 'v10.0.0', annotated: true }, { name: 'v9.0.0', annotated: false }] });
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED'), false);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-HISTORY'), true);
});
// ------------------------------------------------------------ badge honesty
test('a static badge asserting test or build status is a finding', () => {