feat(register): a decided YES about tag history, and app-creator registered

Two changes that belong together: both are the register learning to record a
decision the engine could otherwise only re-report forever.

`tags_lightweight_accepted` — TAG-ANNOTATED-HISTORY (WARN) fires on 13
historical lightweight tags in three repos. A lightweight tag is movable
without a trace and the catalog pins plugins by tag, so the finding is real.
But the only remedy for a published one is force-moving it — the exact act the
check warns about — so the WARN could never be cleared. That is the `titles`
defect one axis over: the gate could not tell "we decided this" from "nobody
looked".

Keyed on tag NAME, never a count: a count stays satisfied the moment one tag is
re-cut and a different, unaccepted one takes its place.

An accepted tag emits `TAG-ANNOTATED-ACCEPTED` at OK naming the tags — it is
not dropped. An exemption is a finding, the rule `readme_desc_match` already
follows; an exception nobody can see reads exactly like a check that silently
stopped running.

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, and an ERROR today. Pre-accepting it would
mean cutting v7.8.0 instead of fixing it makes the finding vanish silently.
The engine enforces this independently — the newest tag cannot be accepted
away even if named, and a test pins that.

`app-creator` → `standalone` — `--refresh` measured 22 on the forge against 21
registered. Unregistered meant zero checks against a repo published on `open/`.
The class is derived, not guessed: no `.claude-plugin/plugin.json`, absent from
the catalog, own remote on `open/` — identical in form to the four existing
`standalone`.

Measured before and after across all 21 clones, every finding code diffed:
- TAG-ANNOTATED-HISTORY 3 WARN -> 0, converted 1:1 to 3 OK. None disappeared.
- TAG-ANNOTATED unchanged at 1 ERROR (ktg-plugin-marketplace v7.7.2).
- app-creator: 1 SKIP -> 16 judged findings (3 ERROR, 2 WARN). Recorded, not
  fixed — it records, it does not fix.
- Nothing else moved.
- `--refresh`: 22/22, no divergence.

The accepted lists are claims about three OTHER repos, measured from LOCAL
clones — which is the gap REMOTE-SYNC exists to name. They go to those repos by
coord so a wrong name can be disputed.

187 tests (was 182).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AkHEqTSr1k3HbeiHu1ggW
This commit is contained in:
Kjell Tore Guttormsen 2026-08-12 22:24:03 +02:00
commit fe0d039de2
3 changed files with 119 additions and 4 deletions

View file

@ -854,6 +854,69 @@ test('a repo with no tags has nothing to judge — OK, not SKIP', () => {
assert.equal(f[0].code, 'TAGS-NONE');
});
// ---------------------------------------- accepted lightweight tag history
//
// The WARN above can never be cleared: the only remedy is force-moving an
// already published ref, which is the act the check warns about. Left there,
// it is the `titles` defect again — the gate cannot tell "we decided this" from
// "nobody has looked". The register records the decision, keyed on TAG NAME
// (a count goes silently wrong the moment one tag is re-cut and another takes
// its place).
const ACCEPT_REG = { tags_lightweight_accepted: { alpha: ['v0.1.0', 'v0.2.0'] } };
test('an accepted lightweight tag emits an OK naming it — an exemption is a finding, not a deletion', () => {
const tagObjects = [
{ name: 'v0.1.0', annotated: false },
{ name: 'v0.2.0', annotated: false },
{ name: 'v1.0.0', annotated: true },
];
const f = checkTagIntegrity({ tagObjects, name: 'alpha' }, ACCEPT_REG);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-HISTORY'), false);
const ok = f.find((x) => x.code === 'TAG-ANNOTATED-ACCEPTED');
assert.equal(ok.level, 'OK');
assert.match(ok.msg, /v0\.1\.0/);
assert.match(ok.msg, /v0\.2\.0/);
});
test('an unaccepted lightweight tag still fires, and the WARN counts only the unaccepted', () => {
const tagObjects = [
{ name: 'v0.1.0', annotated: false }, // accepted
{ name: 'v0.2.0', annotated: false }, // accepted
{ name: 'v0.3.0', annotated: false }, // NOT accepted
{ name: 'v1.0.0', annotated: true },
];
const f = checkTagIntegrity({ tagObjects, name: 'alpha' }, ACCEPT_REG);
const warn = f.find((x) => x.code === 'TAG-ANNOTATED-HISTORY');
assert.equal(warn.level, 'WARN');
assert.match(warn.msg, /v0\.3\.0/);
assert.equal(/v0\.1\.0/.test(warn.msg), false);
assert.match(warn.msg, /\b1 older/);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-ACCEPTED'), true);
});
// 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);
const hit = f.find((x) => x.code === 'TAG-ANNOTATED');
assert.equal(hit.level, 'ERROR');
});
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);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-HISTORY'), true);
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-ACCEPTED'), false);
});
test('no register entry leaves the behaviour exactly as it was', () => {
const tagObjects = [{ name: 'v0.1.0', annotated: false }, { name: 'v1.0.0', annotated: true }];
const f = checkTagIntegrity({ tagObjects, name: 'alpha' }, {});
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-HISTORY'), true);
});
// 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