feat(engine): TAG-SIGNED — a tag that names a signer, not just write access
The check sat parked as "blocked on an operator decision" on the strength of 0 of 18 repos signing. That zero was never evidence against it: it is what a practice nobody has adopted yet looks like, and reasoning from it makes the gate ratify the status quo it exists to move. The rule that settled it asks something else — would a public AAA+ repository do this? Yes, and unlike the two conventions that killed VERSION-DRIFT, there is no second convention under which release tags are better left unattributable. The measurement then decided the SCOPE, not the yes. Across 20 clones: 158 v* tags, 144 annotated, 14 lightweight, 0 signed, and not one dated after 2026-08-12. A boolean "this org signs" would have failed 20 correct repositories on day one with force-moving 144 published refs as its only remedy — the mechanism that gets gates switched off. So the policy is a DATE in the register (never in the engine, the rule that keeps the org-profile exemption a flag): nothing fires today, teeth at each repo's next release. Verified both directions, because a sweep returning zero proves nothing on its own — a dead check returns zero too. 21 repos: 18 PREPOLICY + 3 NONE, 0 ERROR, 0 WARN; then against real git objects an unsigned post-policy newest tag does fire ERROR. Two exclusions keep one defect from being counted twice: a lightweight tag has no object to carry a signature (byDesign skip naming TAG-ANNOTATED as owner), and a pre-policy tag was correct when cut (OK, said out loud, never silence). It answers "is it signed", never "does the forge vouch for it". The README row ships in this commit, not after it, and the stale test count in CLAUDE.md (230 → 241) is corrected here rather than left for the next reader. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsPsVsvhrSaejK3cLPmnN2
This commit is contained in:
parent
9f08ebc5ed
commit
e6cd8983ca
9 changed files with 415 additions and 17 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-standard",
|
||||
"version": "0.10.1",
|
||||
"version": "0.11.0",
|
||||
"description": "Per-repo gate for the open/ presentation standard: README first screen, install block, files required by the repo's class, and dead repo references.",
|
||||
"author": {
|
||||
"name": "Kjell Tore Guttormsen"
|
||||
|
|
|
|||
52
CHANGELOG.md
52
CHANGELOG.md
|
|
@ -6,6 +6,58 @@ versioning is [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.11.0] — 2026-08-13
|
||||
|
||||
### Added
|
||||
|
||||
- **`TAG-SIGNED` — a release tag that names a signer, not just whoever could
|
||||
write to the forge.** The catalog pins plugins by tag, so this sits on the
|
||||
same supply-chain axis as `TAG-ANNOTATED`: annotation makes a tag
|
||||
immovable-without-a-trace, a signature makes it attributable.
|
||||
|
||||
The check had been parked for a release as "blocked on an operator decision",
|
||||
on the strength of 0 of 18 repos signing. That zero was never evidence against
|
||||
it — it is what a practice nobody has adopted yet looks like, and reasoning
|
||||
from it makes the gate ratify the status quo it exists to move. The rule that
|
||||
settled it (operator, 2026-08-13) asks something else entirely: *would a public
|
||||
AAA+ repository do this?* Yes. Unlike the two legitimate conventions that
|
||||
killed `VERSION-DRIFT`, there is no second convention under which release tags
|
||||
are better left unattributable.
|
||||
|
||||
**The policy is a DATE, and the date is what made the check shippable.**
|
||||
Measured across 20 clones before the rule was locked: 158 `v*` tags, 144
|
||||
annotated, 14 lightweight, 0 signed, and not one tag dated after 2026-08-12. A
|
||||
boolean "this org signs" would have failed 20 correct repositories the day it
|
||||
landed, with force-moving 144 published refs as its only remedy — the mechanism
|
||||
that gets gates switched off. `tags_signed_from` lives in the register, never
|
||||
in the engine (the rule that keeps the org-profile exemption a flag rather than
|
||||
a class name in a classifier); absent it, the check is a `notRun` SKIP, because
|
||||
the gate never assumes a policy nobody recorded.
|
||||
|
||||
Verified both directions: a sweep of 21 repos yields 18 `TAG-SIGNED-PREPOLICY`
|
||||
+ 3 `TAG-SIGNED-NONE`, 0 ERROR, 0 WARN — then, against real git objects, an
|
||||
unsigned post-policy newest tag does fire `ERROR`. A sweep returning zero
|
||||
proves nothing alone; a dead check returns zero too.
|
||||
|
||||
Two exclusions keep one defect from being counted twice under two codes: a
|
||||
**lightweight** tag has no tag object to carry a signature at all
|
||||
(`TAG-SIGNED-LIGHTWEIGHT`, a `byDesign` skip naming `TAG-ANNOTATED` as the
|
||||
owner), and a **pre-policy** tag was correct when it was cut
|
||||
(`TAG-SIGNED-PREPOLICY`, an `OK` — said out loud, never as silence).
|
||||
|
||||
It answers *is this tag signed* and never *does the forge vouch for it*: the
|
||||
signature is read from the clone, while the forge's "Verified" badge needs a
|
||||
key registered there.
|
||||
|
||||
### Changed
|
||||
|
||||
- The engine reads tag signature and creation date alongside the object type, in
|
||||
the same single `for-each-ref`. The signature is read with
|
||||
`%(if)%(contents:signature)%(then)…` rather than by printing the block, which
|
||||
is multi-line and would break the line-oriented parser on exactly the tags the
|
||||
check cares about. No new API call — this stays at three.
|
||||
- 241 tests, from 230.
|
||||
|
||||
## [0.10.1] — 2026-08-12
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
48
CLAUDE.md
48
CLAUDE.md
|
|
@ -282,16 +282,42 @@ would recreate, in data, exactly the drift this plugin exists to remove.
|
|||
has no convention on either side because it has no subject. Do not re-derive
|
||||
it from "18/18" alone — the fire rate is a proxy, and reasoning from the proxy
|
||||
is what would resurrect this check the first time one repo publishes an asset.
|
||||
- **`TAG-SIGNED` is BLOCKED ON AN OPERATOR DECISION, not rejected — and filing it
|
||||
with the rejections is the error to avoid.** 0 of 18 repos sign their tags
|
||||
today, which looks like the same measurement that killed VERSION-DRIFT and is
|
||||
not: if the operator decides this org signs tags, the check acquires its whole
|
||||
subject the same day. A rejection is permanent; this is one yes/no away.
|
||||
Recorded here so the next session reads a pending question instead of
|
||||
re-measuring a settled one. The scope is also fixed in advance: `git cat-file
|
||||
tag` reads the signature block from the clone, so the check can answer *is it
|
||||
signed* and must never claim *the forge vouches for it* — the forge's
|
||||
"Verified" badge needs a key registered there.
|
||||
- **The question is "would a public AAA+ repo do this", NOT "how many of ours
|
||||
already do".** Operator rule, 2026-08-13, and it settled `TAG-SIGNED` — which
|
||||
had been parked as "blocked on an operator decision" on the strength of 0 of
|
||||
18 repos signing. That zero was never evidence against the check: it is what a
|
||||
practice nobody has adopted yet looks like, and reasoning from it makes the
|
||||
gate ratify the status quo it exists to move. The operator's word for being
|
||||
asked at all was that it "should almost be self-evident". This is genuinely
|
||||
different from the VERSION-DRIFT rejection, where two legitimate conventions
|
||||
coexisted; there is no second convention under which release tags are better
|
||||
left unattributable. **The test decides WHETHER; the corpus measurement then
|
||||
decides only HOW TO SCOPE** so the rule does not fail correct repositories on
|
||||
day one. Do not let a low adoption rate veto a yes.
|
||||
This does not reopen `RELEASE-ASSETS`, which was rejected for the stronger
|
||||
reason — no subject at all, 0 of 21 READMEs referencing an asset download.
|
||||
- **`TAG-SIGNED` ships as a policy DATE, and the date is what made it
|
||||
shippable.** Measured across 20 clones before the rule was locked (2026-08-13):
|
||||
158 `v*` tags, 144 annotated, 14 lightweight, **0 signed**, and not one tag
|
||||
dated after 2026-08-12. A boolean "this org signs" would have failed 20
|
||||
correct repositories the day it landed, with force-moving 144 published refs
|
||||
as its only remedy — the mechanism that gets gates switched off. The date
|
||||
(`tags_signed_from` in the register, never in the engine, the same rule that
|
||||
keeps the org-profile exemption a flag) fires nothing today and acquires teeth
|
||||
at each repo's next release, which is how signing is adopted in public.
|
||||
Verified by sweep: 21 repos, 18 `TAG-SIGNED-PREPOLICY` + 3 `TAG-SIGNED-NONE`,
|
||||
0 ERROR, 0 WARN — then positively controlled against real git objects, where
|
||||
an unsigned post-policy newest tag does fire `ERROR`. A sweep returning zero
|
||||
proves nothing on its own; a dead check returns zero too.
|
||||
Two exclusions keep one defect from being counted twice: a **lightweight** tag
|
||||
has no tag object to carry a signature (`TAG-ANNOTATED` owns it, and the skip
|
||||
names that owner), and a **pre-policy** tag was correct when it was cut — said
|
||||
out loud as an `OK`, never as silence. Scope fixed in advance and held:
|
||||
the signature is read from the clone, so the check answers *is it signed* and
|
||||
never *does the forge vouch for it* — that badge needs a key registered there.
|
||||
Read with `%(if)%(contents:signature)%(then)…` rather than by printing the
|
||||
block, which is multi-line and would break the line-oriented parser on exactly
|
||||
the tags that matter.
|
||||
- **A gate whose subject varies per repo MEASURES the subject; it does not read
|
||||
it off the class.** `VERIFY-COMMAND` fires only where something runnable
|
||||
exists (`scripts.test`, or a tracked executable test file), and answers
|
||||
|
|
@ -318,7 +344,7 @@ would recreate, in data, exactly the drift this plugin exists to remove.
|
|||
## Commands
|
||||
|
||||
```bash
|
||||
npm test # 230 tests
|
||||
npm test # 241 tests
|
||||
node scripts/repo-standard-check.mjs --dir "$PWD" # gate one repo
|
||||
node scripts/repo-standard-check.mjs --offline # no network call
|
||||
node scripts/repo-standard-check.mjs --json # machine output
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ checks that surface in one repository and reports what it finds.
|
|||
|
||||
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
|
@ -80,6 +80,7 @@ The repository's **class** decides what each check means:
|
|||
| Boilerplate | template text nobody filled in |
|
||||
| Version consistency | manifest, README badge, newest CHANGELOG entry and the git tag disagree |
|
||||
| Tag integrity | the newest tag is lightweight — movable without a trace, under a catalog that pins by tag. Older ones are exposed once as a count, because their only "fix" is force-moving a published ref |
|
||||
| Tag signing | a tag cut on or after the org's signing date carries no signature, so nothing ties the released ref to a signer rather than to whoever could write to the forge. Tags predating the date are never judged — signing them now would mean force-moving published refs. Answers *is it signed*, never *does the forge vouch for it* |
|
||||
| Release current | the newest release the forge publishes is older than the newest tag it lists, so the version page shows a version older than the code. `WARN` — publishing a release for an existing tag moves nothing |
|
||||
| Remote sync | a tag exists only in this clone. The manifest can read as correctly tagged while no stranger can resolve the version |
|
||||
| Verify command | the repo has something runnable and the README shows no command to run it. With no CI badge in this org, that command is the only evidence a stranger gets |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "repo-standard",
|
||||
"version": "0.10.1",
|
||||
"version": "0.11.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -214,6 +214,31 @@
|
|||
"llm-ingestion-pipeline-security": ["v0.3.0"]
|
||||
},
|
||||
|
||||
"$comment_tags_signed_from": [
|
||||
"The date the org's tag-signing policy takes effect. Operator decision",
|
||||
"2026-08-13, on the rule that decides these: would a public AAA+ repository",
|
||||
"do this? Yes — a signed tag ties a released ref to a signer rather than to",
|
||||
"whoever could write to the forge, and the catalog pins plugins by tag.",
|
||||
"",
|
||||
"It lives HERE and not in the engine for the reason the org-profile",
|
||||
"exemption is a flag rather than `if (klass === 'org-profile')`: the engine",
|
||||
"holds no org decision of its own. Remove the key and TAG-SIGNED goes back",
|
||||
"to a notRun SKIP — the gate never assumes a policy nobody recorded.",
|
||||
"",
|
||||
"A DATE, not a boolean, and that is the whole reason the check is shippable.",
|
||||
"MEASURED across 20 clones 2026-08-13: 158 `v*` tags, 144 annotated, 14",
|
||||
"lightweight, 0 signed, and NOT ONE tag dated after 2026-08-12. A boolean",
|
||||
"would have failed 20 correct repositories the day it landed — the mechanism",
|
||||
"that gets gates switched off — and its only remedy would have been force-",
|
||||
"moving 144 published refs. The date fires nothing today and acquires teeth",
|
||||
"at each repo's next release, which is how signing is adopted in public.",
|
||||
"",
|
||||
"Do NOT backdate this to 'catch up' history. Every tag below it was correct",
|
||||
"when it was cut, and the check says so out loud (TAG-SIGNED-PREPOLICY, OK)",
|
||||
"rather than going silent about it."
|
||||
],
|
||||
"tags_signed_from": "2026-08-13",
|
||||
|
||||
"description_max_codepoints": 180,
|
||||
"$comment_length": [
|
||||
"180 codepoints, not bytes and not UTF-16 units. The same string measures 248",
|
||||
|
|
|
|||
|
|
@ -737,6 +737,117 @@ export function checkTagIntegrity({ tagObjects, name }, register) {
|
|||
return findings;
|
||||
}
|
||||
|
||||
// A signed tag is the one thing that ties a released ref to a person rather
|
||||
// than to whoever could write to the forge. The catalog pins plugins by tag, so
|
||||
// this sits on the same supply-chain axis as TAG-ANNOTATED: annotation makes a
|
||||
// tag immovable-without-a-trace, a signature makes it attributable.
|
||||
//
|
||||
// WHY it exists is not the adoption rate. 0 of 158 tags in the org are signed —
|
||||
// which is exactly what a practice nobody has adopted yet looks like, and
|
||||
// reasoning from that zero would have the gate ratify the status quo it exists
|
||||
// to move. The operator's rule (2026-08-13) is the one that decided it: would a
|
||||
// public AAA+ repository do this? Yes. That is a different question from the one
|
||||
// that rejected VERSION-DRIFT, where two legitimate conventions genuinely
|
||||
// coexisted; there is no second convention under which release tags are better
|
||||
// unattributable.
|
||||
//
|
||||
// The measurement decides the SCOPE. Across 20 clones: 158 `v*` tags, 144
|
||||
// annotated, 14 lightweight, 0 signed, and not one tag dated later than
|
||||
// 2026-08-12. So the policy is a DATE held in the register: zero findings on the
|
||||
// day it lands, teeth at each repo's next release. That is how signing is
|
||||
// adopted in public — never by rewriting published history, which is the same
|
||||
// unsafe remedy TAG-ANNOTATED refuses to demand.
|
||||
//
|
||||
// The date lives in the register and NOT in this file, for the reason the
|
||||
// org-profile exemption is a flag rather than `if (klass === 'org-profile')`:
|
||||
// the engine holds no org decision of its own. Absent, the check is a notRun
|
||||
// SKIP — an operator adding the key turns it into a verdict.
|
||||
//
|
||||
// Two exclusions keep one defect from being counted twice under two codes:
|
||||
// - a LIGHTWEIGHT tag has no tag object to carry a signature, so there is no
|
||||
// version of it that passes while staying lightweight — TAG-ANNOTATED owns
|
||||
// it, and the skip names that owner instead of going quiet.
|
||||
// - a tag cut BEFORE the policy was correct when it was cut.
|
||||
//
|
||||
// Read from local git objects, so it costs nothing against the API budget. It
|
||||
// answers *is it signed* and never *does the forge vouch for it* — that badge
|
||||
// needs a key registered there, which is a different subject on a different
|
||||
// surface.
|
||||
export function checkTagSigned({ tagObjects }, register) {
|
||||
const from = register?.tags_signed_from;
|
||||
if (!from) {
|
||||
return [{
|
||||
level: 'SKIP',
|
||||
skip: 'notRun',
|
||||
code: 'TAG-SIGNED-NOPOLICY',
|
||||
msg: 'the register records no tag-signing policy (`tags_signed_from`) — whether this org signs its tags is an operator decision, not one the gate makes on its own',
|
||||
}];
|
||||
}
|
||||
|
||||
const tags = [...(tagObjects ?? [])].sort((a, b) => compareTags(a.name, b.name));
|
||||
if (tags.length === 0) {
|
||||
// The TAGS-NONE / VERSION-NONE shape: the check ran, saw every tag there is,
|
||||
// and found no subject. That is a verdict, not an absent one.
|
||||
return [{ level: 'OK', code: 'TAG-SIGNED-NONE', msg: 'repo has no version tags — no tag exists that could carry a signature' }];
|
||||
}
|
||||
|
||||
const findings = [];
|
||||
// ISO dates compare correctly as strings, which also keeps this free of any
|
||||
// clock reading — the policy boundary is data, never "now".
|
||||
const inScope = tags.filter((t) => t.annotated && t.date && t.date >= from);
|
||||
const prePolicy = tags.filter((t) => t.date && t.date < from);
|
||||
const lightweight = tags.filter((t) => !t.annotated && t.date && t.date >= from);
|
||||
|
||||
if (inScope.length > 0) {
|
||||
const newest = inScope[inScope.length - 1];
|
||||
if (!newest.signed) {
|
||||
findings.push({
|
||||
level: 'ERROR',
|
||||
code: 'TAG-SIGNED',
|
||||
bucket: 'weakening',
|
||||
msg: `newest tag \`${newest.name}\` was cut under the signing policy (from ${from}) but carries no signature — nothing ties it to a signer rather than to whoever could write to the forge, and the catalog pins releases by tag. Re-cut it signed: \`git tag -s -f ${newest.name} ${newest.name}^{}\`.`,
|
||||
});
|
||||
}
|
||||
const older = inScope.slice(0, -1).filter((t) => !t.signed);
|
||||
if (older.length > 0) {
|
||||
findings.push({
|
||||
level: 'WARN',
|
||||
code: 'TAG-SIGNED-HISTORY',
|
||||
bucket: 'weakening',
|
||||
msg: `${older.length} older unsigned tag(s) cut under the policy (${older.slice(0, 3).map((t) => t.name).join(', ')}${older.length > 3 ? ', …' : ''}). WARN, not ERROR: the only remedy is force-moving an already published ref, which is its own risk. Sign every NEW tag (\`git tag -s\`).`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// An exemption is a finding, not a deletion — the rule `readme_desc_match`
|
||||
// and TAG-ANNOTATED-ACCEPTED already follow. An exception nobody can see
|
||||
// reads exactly like a check that silently stopped running.
|
||||
if (prePolicy.length > 0) {
|
||||
findings.push({
|
||||
level: 'OK',
|
||||
code: 'TAG-SIGNED-PREPOLICY',
|
||||
msg: `${prePolicy.length} tag(s) predate the signing policy (from ${from}) and are not judged — they were correct when they were cut, and signing them now would mean force-moving published refs. Every tag from ${from} onward is judged.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (lightweight.length > 0) {
|
||||
findings.push({
|
||||
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 that finding; this check declines rather than report one defect twice.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (findings.length === 0 && inScope.length > 0) {
|
||||
findings.push({ level: 'OK', code: 'TAGS-SIGNED', msg: `all ${inScope.length} tag(s) cut under the policy (from ${from}) are signed` });
|
||||
}
|
||||
if (findings.length === 0) {
|
||||
findings.push({ level: 'OK', code: 'TAGS-SIGNED', msg: `no tag has been cut since the signing policy took effect (${from}) — nothing to judge yet` });
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
// A static image asserting "tests: 642 passing" is a claim dressed as evidence.
|
||||
// Version, licence and platform badges assert no run, so they are fine static.
|
||||
// Bare `status` used to be in this list and caught a self-declared maturity
|
||||
|
|
@ -1417,6 +1528,7 @@ export function classifyRepo(
|
|||
...checkBoilerplate({ files }),
|
||||
...checkVersionConsistency({ pluginVersion, readmeBadge, changelogTop, tags }),
|
||||
...checkTagIntegrity({ tagObjects, name }, register),
|
||||
...checkTagSigned({ tagObjects }, register),
|
||||
...checkReleaseCurrent({ forgeTagsSelf, releases }),
|
||||
...checkRemoteSync({ tags, forgeTagsSelf }),
|
||||
...checkVerifyCommand({ readme, testScript, testFileCount }),
|
||||
|
|
@ -1678,13 +1790,21 @@ function forgeTags(register, repo) {
|
|||
}
|
||||
}
|
||||
|
||||
// The signature is read with `%(if)%(contents:signature)%(then)…`, not by
|
||||
// printing the block itself: a PGP or SSH signature is multi-line, and emitting
|
||||
// it would break a line-oriented parser on exactly the tags the check cares
|
||||
// about. Git answers the yes/no question directly, so one process still covers
|
||||
// every tag. `%(creatordate:short)` is the tag's own date for an annotated tag
|
||||
// and the commit's for a lightweight one — ISO, so the policy comparison is a
|
||||
// string compare with no clock reading anywhere.
|
||||
function gitTagObjects(dir) {
|
||||
try {
|
||||
return execFileSync('git', ['-C', dir, 'for-each-ref', '--format=%(objecttype) %(refname:short)', 'refs/tags/v*'], { encoding: 'utf8' })
|
||||
const format = '%(objecttype) %(if)%(contents:signature)%(then)signed%(else)unsigned%(end) %(creatordate:short) %(refname:short)';
|
||||
return execFileSync('git', ['-C', dir, 'for-each-ref', `--format=${format}`, 'refs/tags/v*'], { encoding: 'utf8' })
|
||||
.split('\n').map((s) => s.trim()).filter(Boolean)
|
||||
.map((line) => {
|
||||
const [type, ...rest] = line.split(' ');
|
||||
return { name: rest.join(' '), annotated: type === 'tag' };
|
||||
const [type, sig, date, ...rest] = line.split(' ');
|
||||
return { name: rest.join(' '), annotated: type === 'tag', signed: sig === 'signed', date };
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
checkHeadings,
|
||||
checkBadges,
|
||||
checkTagIntegrity,
|
||||
checkTagSigned,
|
||||
checkInstallPins,
|
||||
extractInstallPins,
|
||||
checkReadmeLanguage,
|
||||
|
|
@ -934,6 +935,148 @@ test('newest is the highest version, not the last element of an unsorted list',
|
|||
assert.equal(f.some((x) => x.code === 'TAG-ANNOTATED-HISTORY'), true);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------- tag signing
|
||||
//
|
||||
// Built on an operator decision (2026-08-13): the org signs tags FORWARD. The
|
||||
// decision rule was not the adoption rate — it was "would a public AAA+ repo do
|
||||
// this", which is a yes. 0 of 158 signed is what you expect of a practice not
|
||||
// yet adopted, and letting that zero veto the check would make the gate ratify
|
||||
// the status quo it exists to move.
|
||||
//
|
||||
// The measurement decides the SCOPE, not the yes. Across 20 clones: 158 `v*`
|
||||
// tags, 144 annotated, 14 lightweight, 0 signed, and NOT ONE tag dated after the
|
||||
// decision. So a policy DATE fires zero findings on day one and acquires teeth
|
||||
// at each repo's next release — which is how signing is actually adopted, never
|
||||
// by rewriting published history.
|
||||
//
|
||||
// Two exclusions keep this from double-counting a defect that already has an
|
||||
// owner: a lightweight tag has no tag object to carry a signature at all
|
||||
// (TAG-ANNOTATED owns it), and a tag predating the policy was correct when it
|
||||
// was cut.
|
||||
const SIGN_REG = { tags_signed_from: '2026-08-13' };
|
||||
|
||||
test('an unsigned newest tag cut under the policy is an ERROR', () => {
|
||||
const f = checkTagSigned({ tagObjects: [
|
||||
{ name: 'v1.0.0', annotated: true, signed: true, date: '2026-08-14' },
|
||||
{ name: 'v1.1.0', annotated: true, signed: false, date: '2026-08-15' },
|
||||
] }, SIGN_REG);
|
||||
const hit = f.find((x) => x.code === 'TAG-SIGNED');
|
||||
assert.equal(hit.level, 'ERROR');
|
||||
assert.equal(hit.bucket, 'weakening');
|
||||
assert.match(hit.msg, /v1\.1\.0/);
|
||||
});
|
||||
|
||||
test('all in-scope tags signed is an OK', () => {
|
||||
const f = checkTagSigned({ tagObjects: [
|
||||
{ name: 'v1.0.0', annotated: true, signed: true, date: '2026-08-14' },
|
||||
{ name: 'v1.1.0', annotated: true, signed: true, date: '2026-08-15' },
|
||||
] }, SIGN_REG);
|
||||
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
|
||||
assert.equal(f.some((x) => x.code === 'TAGS-SIGNED'), true);
|
||||
});
|
||||
|
||||
// The same split TAG-ANNOTATED makes, for the same reason: the newest can be
|
||||
// re-cut, the older ones can only be "fixed" by force-moving a published ref.
|
||||
test('older unsigned in-scope tags are ONE aggregated WARN, never one per tag', () => {
|
||||
const f = checkTagSigned({ tagObjects: [
|
||||
{ name: 'v1.0.0', annotated: true, signed: false, date: '2026-08-14' },
|
||||
{ name: 'v1.1.0', annotated: true, signed: false, date: '2026-08-15' },
|
||||
{ name: 'v1.2.0', annotated: true, signed: true, date: '2026-08-16' },
|
||||
] }, SIGN_REG);
|
||||
const warns = f.filter((x) => x.code === 'TAG-SIGNED-HISTORY');
|
||||
assert.equal(warns.length, 1);
|
||||
assert.equal(warns[0].level, 'WARN');
|
||||
assert.match(warns[0].msg, /2/);
|
||||
assert.equal(f.some((x) => x.level === 'ERROR'), false);
|
||||
});
|
||||
|
||||
// This is the test that keeps the check from failing 20 correct repositories on
|
||||
// the day the policy lands. Every tag in the corpus predates it.
|
||||
test('tags predating the policy are never judged — and say so as an OK, not silence', () => {
|
||||
const f = checkTagSigned({ tagObjects: [
|
||||
{ name: 'v0.9.0', annotated: true, signed: false, date: '2026-08-09' },
|
||||
{ name: 'v1.0.0', annotated: true, signed: false, date: '2026-08-12' },
|
||||
] }, SIGN_REG);
|
||||
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
|
||||
const ok = f.find((x) => x.code === 'TAG-SIGNED-PREPOLICY');
|
||||
assert.equal(ok.level, 'OK');
|
||||
assert.match(ok.msg, /2/);
|
||||
assert.match(ok.msg, /2026-08-13/);
|
||||
});
|
||||
|
||||
test('a tag cut exactly ON the policy date is in scope — the policy starts that day', () => {
|
||||
const f = checkTagSigned({ tagObjects: [
|
||||
{ name: 'v1.0.0', annotated: true, signed: false, date: '2026-08-13' },
|
||||
] }, SIGN_REG);
|
||||
assert.equal(f.find((x) => x.code === 'TAG-SIGNED').level, 'ERROR');
|
||||
});
|
||||
|
||||
// A lightweight tag has no tag object, so it cannot carry a signature — there
|
||||
// is no version of it that passes this check while staying lightweight. Firing
|
||||
// here would report ONE defect twice under two codes; TAG-ANNOTATED already
|
||||
// owns it, and the skip names that owner rather than going quiet.
|
||||
test('a lightweight tag is a byDesign SKIP that names TAG-ANNOTATED as the owner', () => {
|
||||
const f = checkTagSigned({ tagObjects: [
|
||||
{ name: 'v1.0.0', annotated: false, signed: false, date: '2026-08-14' },
|
||||
] }, SIGN_REG);
|
||||
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
|
||||
const skip = f.find((x) => x.code === 'TAG-SIGNED-LIGHTWEIGHT');
|
||||
assert.equal(skip.level, 'SKIP');
|
||||
assert.equal(skip.skip, 'byDesign');
|
||||
assert.match(skip.msg, /TAG-ANNOTATED/);
|
||||
});
|
||||
|
||||
// The engine must hold no org decision of its own — the same rule that keeps
|
||||
// the org-profile exemption a register flag rather than `if (klass === …)`.
|
||||
// notRun, not byDesign: an operator adding the key turns this into a verdict.
|
||||
test('no policy in the register is a notRun SKIP — the engine never assumes the org signs', () => {
|
||||
const f = checkTagSigned({ tagObjects: [
|
||||
{ name: 'v1.0.0', annotated: true, signed: false, date: '2026-08-14' },
|
||||
] }, {});
|
||||
assert.equal(f.length, 1);
|
||||
assert.equal(f[0].level, 'SKIP');
|
||||
assert.equal(f[0].skip, 'notRun');
|
||||
assert.equal(f[0].code, 'TAG-SIGNED-NOPOLICY');
|
||||
});
|
||||
|
||||
test('a repo with no tags has nothing to judge — OK, not SKIP', () => {
|
||||
const f = checkTagSigned({ tagObjects: [] }, SIGN_REG);
|
||||
assert.equal(f.length, 1);
|
||||
assert.equal(f[0].level, 'OK');
|
||||
assert.equal(f[0].code, 'TAG-SIGNED-NONE');
|
||||
});
|
||||
|
||||
// An undatable tag cannot be PROVEN to fall under the policy, and a gate that
|
||||
// fails a correct repository on a missing field is the mechanism that gets
|
||||
// gates switched off. Absence falls to the safe side here — unlike an untagged
|
||||
// SKIP, where the loud side is the safe one.
|
||||
test('a tag with no readable date is out of scope, never assumed post-policy', () => {
|
||||
const f = checkTagSigned({ tagObjects: [
|
||||
{ name: 'v1.0.0', annotated: true, signed: false },
|
||||
] }, SIGN_REG);
|
||||
assert.equal(f.some((x) => x.level === 'ERROR' || x.level === 'WARN'), false);
|
||||
});
|
||||
|
||||
// CLAUDE.md fixes this scope in advance: `git cat-file tag` reads the signature
|
||||
// block from the clone, so the check answers *is it signed* and must never
|
||||
// claim the forge vouches for it — that needs a key registered there.
|
||||
test('no finding claims the forge verified anything', () => {
|
||||
const f = checkTagSigned({ tagObjects: [
|
||||
{ name: 'v1.0.0', annotated: true, signed: false, date: '2026-08-14' },
|
||||
{ name: 'v1.1.0', annotated: true, signed: false, date: '2026-08-15' },
|
||||
] }, SIGN_REG);
|
||||
for (const x of f) assert.equal(/verified|vouch|trusted|valid signature/i.test(x.msg), false);
|
||||
});
|
||||
|
||||
test('newest in scope is the highest version, not the last element of an unsorted list', () => {
|
||||
const f = checkTagSigned({ tagObjects: [
|
||||
{ name: 'v10.0.0', annotated: true, signed: true, date: '2026-08-14' },
|
||||
{ name: 'v9.0.0', annotated: true, signed: false, date: '2026-08-15' },
|
||||
] }, SIGN_REG);
|
||||
assert.equal(f.some((x) => x.code === 'TAG-SIGNED'), false);
|
||||
assert.equal(f.some((x) => x.code === 'TAG-SIGNED-HISTORY'), true);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------- dead pins
|
||||
//
|
||||
// Reported by org-ops (census 08) and re-measured here against the FORGE, not
|
||||
|
|
|
|||
|
|
@ -175,6 +175,37 @@ never *passing*.
|
|||
domain packs owes no command, and adding a hollow test script to clear a finding
|
||||
that was never against it is the anti-pattern in its purest form.
|
||||
|
||||
## Tag signing — forward only, and never by rewriting history
|
||||
|
||||
Since 0.11.0 the gate judges whether a release tag is signed. The org's policy
|
||||
date lives in the register (`tags_signed_from`); tags cut before it are reported
|
||||
as `TAG-SIGNED-PREPOLICY` (`OK`) and are **not** findings. If the key is absent
|
||||
the check is a `notRun` SKIP — the gate never assumes a policy nobody recorded.
|
||||
|
||||
`TAG-SIGNED` (`ERROR`) is the newest tag cut under the policy, unsigned. The
|
||||
remedy is to re-cut that one tag signed. `TAG-SIGNED-HISTORY` (`WARN`) is a
|
||||
count of older ones, and it is a `WARN` precisely because its only remedy is
|
||||
force-moving an already published ref — the risk the check exists to name. Do
|
||||
not clear a WARN by doing the dangerous thing.
|
||||
|
||||
Two moves look like fixes and are not:
|
||||
|
||||
- **Backdating `tags_signed_from` to "catch up".** Every tag below the date was
|
||||
correct when it was cut. Backdating manufactures findings whose only remedy is
|
||||
re-signing published history.
|
||||
- **Re-signing the whole tag history.** Same act, one step further. The org
|
||||
adopted signing forward for the same reason public projects do.
|
||||
|
||||
`TAG-SIGNED-LIGHTWEIGHT` is a `byDesign` skip, not work. A lightweight tag has
|
||||
no tag object to hold a signature at all, so signing is not a remedy it has —
|
||||
`TAG-ANNOTATED` owns that finding, and fixing it there is what puts the tag in
|
||||
this check's scope.
|
||||
|
||||
The check reads the signature from the clone, so a green line means *this tag
|
||||
carries a signature*. It does **not** mean the forge verified it — the forge's
|
||||
"Verified" badge needs a key registered there, which is a different subject on a
|
||||
different surface. Do not upgrade the claim when reporting.
|
||||
|
||||
## Diagrams — capability is not justification
|
||||
|
||||
Mermaid renders in a README on this forge. That was verified on the live
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue