Skip to content

Commit 8829aa0

Browse files
committed
fix(skills-ratchet): price the release stamp's prerelease segment at stable width
The skills token ratchet counted the `metadata.version` stamp that the version script writes into every published SKILL.md as authored text. Under Changesets pre mode the stamp grows from "17.7.0" to "18.0.0-next.0" (+7 bytes), so objectstack-automation (1 token of headroom) and objectstack-data (0) went red on the Version Packages PR with no author edit to pay for. The gate now prices that one stamp at its stable width: the semver prerelease segment of `metadata.version` is not counted, for the files the version script stamps. The stamp is read with check-skill-compatibility-version's own extractMetadataVersion and the stamped set is its stampedSkillPaths(). Nothing else moves: no ceiling changes, a stable tree's reading is byte-identical, and authored growth under either stamp is still red. Eleven self-test rows pin it; the header's reproducibility note gives the hand audit for a prerelease tree. Claude-Session: https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx Co-authored-by: Claude <noreply@anthropic.com>
1 parent e540e7f commit 8829aa0

1 file changed

Lines changed: 122 additions & 6 deletions

File tree

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@
6161
* IDIOM note at the top of `.github/workflows/lint.yml` carries that lane's
6262
* verbatim scope, and it is pointed at rather than restated here.
6363
* - INDEPENDENTLY REPRODUCIBLE. Anyone can audit a ceiling without running
64-
* this script: `ceil($(wc -c < file) / 4)`. A tokenizer's count can only be
65-
* checked by re-running the tokenizer.
64+
* this script: `ceil($(wc -c < file) / 4)` — on a prerelease tree, first
65+
* subtract the release stamp's prerelease bytes (the one exclusion below).
66+
* A tokenizer's count can only be checked by re-running the tokenizer.
6667
*
6768
* ~4 bytes per token is the standard rule of thumb for BPE over English prose
6869
* and markdown, which is what this catalog is. Its KNOWN limitation, stated
@@ -73,6 +74,26 @@
7374
* the catalog ever turns substantially non-Latin, the convention needs
7475
* revisiting rather than the ceilings.
7576
*
77+
* ONE EXCLUSION — the release stamp's prerelease segment (maintainer ruling,
78+
* route A of decision record 6138951701, verbatim: 「A 剥预发布后缀 (Recommended)」).
79+
* A published SKILL.md's frontmatter `metadata.version` is not authored text:
80+
* the root `version` script writes it from `@objectstack/spec`'s version
81+
* (`scripts/check-skill-compatibility-version.mjs --fix`, whose check reds on
82+
* any other value). Under Changesets pre mode that stamp grows from `"17.7.0"`
83+
* to `"18.0.0-next.0"` — 7 bytes no author wrote and no author can pay for by
84+
* deleting text — so a file at zero headroom went red on the Version Packages
85+
* PR alone. The semver prerelease segment of that ONE stamp (`-next.0`,
86+
* `-next.12`) is therefore not priced: the stamp counts at its stable width,
87+
* the width every ceiling was measured at. Nothing else moves — a
88+
* prerelease-shaped string anywhere else in a file, a stamp-shaped line in a
89+
* file the version script does not stamp, and every byte of a stable tree
90+
* (so `main`'s reading) are priced exactly as before. Where the stamp is, and
91+
* which files carry one, are read through that script's own
92+
* `extractMetadataVersion` and `stampedSkillPaths`, never a second parser here.
93+
* Hand audit on a prerelease tree: `ceil(($(wc -c < file) - N) / 4)`, where N
94+
* is the byte length of the segment after the stamp's patch number (`-next.0`
95+
* is 7, `-next.12` is 8); on a stable stamp N is 0.
96+
*
7697
* ⚠️ Counts produced here are comparable ONLY against other counts produced
7798
* here. They are not an estimate of any specific model's tokenizer, and the
7899
* ceilings below are pinned in this convention — changing the convention
@@ -172,6 +193,9 @@ import { isEntrypoint } from './invoked-as.mjs';
172193
// see the boundary section in the header. #11705's ruling forbids a second
173194
// mechanism, so this gate consults the register the governed-merge fork owns.
174195
import { generatedExceptionFor } from './pm/check-governed-merges.mjs';
196+
// The stamp's ONE reader and the stamped set, from the script that writes the
197+
// stamp — see "ONE EXCLUSION" in the header.
198+
import { extractMetadataVersion, stampedSkillPaths } from './check-skill-compatibility-version.mjs';
175199

176200
const HERE = resolve(fileURLToPath(import.meta.url), '..');
177201
const REPO_ROOT = resolve(HERE, '..');
@@ -595,6 +619,38 @@ export function countTokens(text) {
595619
return Math.ceil(Buffer.byteLength(text, 'utf8') / 4);
596620
}
597621

622+
/** A semver version carrying a prerelease segment; group 1 is the segment. Build metadata never matches. */
623+
const STAMP_PRERELEASE_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z][0-9A-Za-z.-]*)$/;
624+
625+
/**
626+
* The prerelease segment of a file's frontmatter `metadata.version` stamp
627+
* (`-next.0` of `18.0.0-next.0`), or `''` when the file has no stamp or a
628+
* stable one. Read with the stamp writer's own parser — see "ONE EXCLUSION".
629+
*
630+
* @param {string} text
631+
* @returns {string}
632+
*/
633+
export function stampPrerelease(text) {
634+
const stamp = extractMetadataVersion(text);
635+
if (!stamp.ok) return '';
636+
return STAMP_PRERELEASE_RE.exec(stamp.value)?.[1] ?? '';
637+
}
638+
639+
/**
640+
* The count the gate judges: `countTokens`, less the stamp's prerelease
641+
* segment when the file is one the version script stamps. Every other file,
642+
* and every file on a stable tree, prices byte-for-byte as `countTokens`.
643+
*
644+
* @param {string} text
645+
* @param {{stamped?: boolean}} [options] is this file in `stampedSkillPaths()`?
646+
* @returns {number}
647+
*/
648+
export function pricedTokens(text, { stamped = false } = {}) {
649+
if (text.length === 0) return 0;
650+
const unpriced = stamped ? Buffer.byteLength(stampPrerelease(text), 'utf8') : 0;
651+
return Math.ceil((Buffer.byteLength(text, 'utf8') - unpriced) / 4);
652+
}
653+
598654
/**
599655
* Every file that ships inside a published skill — read off the filesystem,
600656
* never a hand list, walked RECURSIVELY (#12392).
@@ -749,6 +805,7 @@ export function reportLines(rows) {
749805

750806
function run() {
751807
const covered = [...new Set([...discoverBundleFiles(), ...CEILINGS.keys()])].sort();
808+
const stampedSet = new Set(stampedSkillPaths());
752809
const rows = [];
753810
let failed = 0;
754811

@@ -761,17 +818,24 @@ function run() {
761818
failed++;
762819
continue;
763820
}
764-
const tokens = countTokens(text);
821+
const stamped = stampedSet.has(rel);
822+
const tokens = pricedTokens(text, { stamped });
823+
const segment = stamped ? stampPrerelease(text) : '';
824+
// Empty on a stable tree, so `main`'s lines are byte-identical to before.
825+
const note = segment
826+
? ` (the \`${segment}\` prerelease segment of its metadata.version stamp, `
827+
+ `${Buffer.byteLength(segment, 'utf8')} bytes, is not priced)`
828+
: '';
765829
const ceiling = CEILINGS.get(rel);
766830
const generated = generatorOwning(rel);
767831
rows.push({ rel, tokens, ceiling, generated });
768832
const v = verdict(rel, tokens, ceiling, generated);
769833
if (!v.ok) {
770834
failed++;
771-
console.error(`✗ check-skills-token-ratchet: ${v.msg}`);
835+
console.error(`✗ check-skills-token-ratchet: ${v.msg}${note}`);
772836
continue;
773837
}
774-
console.log(`✓ check-skills-token-ratchet: ${v.msg}`);
838+
console.log(`✓ check-skills-token-ratchet: ${v.msg}${note}`);
775839
}
776840

777841
const ratcheted = rows.filter((r) => !r.generated).length;
@@ -866,6 +930,17 @@ const SELF_TEST_BATTERIES = Object.freeze({
866930
'five ASCII bytes round up to two': 1,
867931
'the unit is UTF-8 BYTES, not JS characters (a 3-byte CJK char is not free)': 1,
868932
'empty text counts zero, which the verdict turns red': 1,
933+
'the stamp\'s prerelease segment is read off metadata.version': 1,
934+
'a zero-headroom SKILL.md stamped -next.0 is green: the stamp prices at stable width': 1,
935+
'priced whole, that same file is red (the row above is not vacuous)': 1,
936+
'one authored byte over a zero-headroom ceiling is red under a stable stamp': 1,
937+
'one authored byte over is red under a prerelease stamp too: the strip buys an author nothing': 1,
938+
'a multi-digit prerelease (-next.12) prices the same as a stable stamp': 1,
939+
'a stable stamp prices byte-for-byte as countTokens': 1,
940+
'every real bundle file without a prerelease stamp prices exactly as countTokens': 1,
941+
'a prerelease-shaped string outside the stamp is priced': 1,
942+
'a file the version script does not stamp is priced whole': 1,
943+
'the stamped set is the version script\'s: exactly the real SKILL.md files': 1,
869944
'the catalog is discovered from disk': 1,
870945
'every discovered file lives under a published skill directory': 1,
871946
'a discovered file with no ceiling is RED': 1,
@@ -920,7 +995,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
920995
// the literal above, so the roster falls below this number; the table
921996
// cross-check in the floor block is the other half, and names WHICH label
922997
// collided.
923-
const SELF_TEST_BATTERY_FLOOR = 64;
998+
const SELF_TEST_BATTERY_FLOOR = 75;
924999

9251000
function selfTest() {
9261001
const rel = 'skills/objectstack-ui/SKILL.md';
@@ -955,6 +1030,25 @@ function selfTest() {
9551030
// and the case below asks only that every one of them is priced.
9561031
const skillMdOnDisk = discoverBundleFiles().filter((p) => /^skills\/[^/]+\/SKILL\.md$/.test(p)).length;
9571032

1033+
// The release-stamp fixtures ("ONE EXCLUSION" in the header). Padded so the
1034+
// STABLE spelling is a whole number of tokens: a ceiling set at its count has
1035+
// zero headroom, so one more byte anywhere is red — the row that proves the
1036+
// padding is right is the "one authored byte over" pair below.
1037+
const stampHead = (v) => `---\nname: x\nmetadata:\n author: objectstack-ai\n version: "${v}"\n---\n\n# body\n`;
1038+
const stampPad = '.'.repeat((4 - (Buffer.byteLength(stampHead('18.0.0'), 'utf8') % 4)) % 4);
1039+
const stampFixture = (v, extra = '') => `${stampHead(v)}${stampPad}${extra}`;
1040+
const stableStamp = stampFixture('18.0.0');
1041+
const next0Stamp = stampFixture('18.0.0-next.0');
1042+
const zeroHeadroom = countTokens(stableStamp);
1043+
const proseStamp = stampFixture('18.0.0', 'pin 18.0.0-next.0 in prose\n');
1044+
const stampedOnDisk = new Set(stampedSkillPaths());
1045+
const repricedOnStableStamp = discovered.filter((p) => {
1046+
const text = readFileSync(join(REPO_ROOT, p), 'utf8');
1047+
const stamped = stampedOnDisk.has(p);
1048+
if (stamped && stampPrerelease(text) !== '') return false;
1049+
return pricedTokens(text, { stamped }) !== countTokens(text);
1050+
});
1051+
9581052
const cases = [
9591053
// ── ratchet semantics ────────────────────────────────────────────────
9601054
['under the ceiling -> green', verdict(rel, 25000, 25154).ok, true],
@@ -989,6 +1083,28 @@ function selfTest() {
9891083
['the unit is UTF-8 BYTES, not JS characters (a 3-byte CJK char is not free)', countTokens('中'), 1],
9901084
['empty text counts zero, which the verdict turns red', countTokens(''), 0],
9911085

1086+
// ── the release stamp's prerelease segment (ruling record 6138951701) ──
1087+
['the stamp\'s prerelease segment is read off metadata.version', stampPrerelease(next0Stamp), '-next.0'],
1088+
['a zero-headroom SKILL.md stamped -next.0 is green: the stamp prices at stable width',
1089+
verdict(rel, pricedTokens(next0Stamp, { stamped: true }), zeroHeadroom).ok, true],
1090+
['priced whole, that same file is red (the row above is not vacuous)',
1091+
verdict(rel, countTokens(next0Stamp), zeroHeadroom).ok, false],
1092+
['one authored byte over a zero-headroom ceiling is red under a stable stamp',
1093+
verdict(rel, pricedTokens(stampFixture('18.0.0', 'x'), { stamped: true }), zeroHeadroom).ok, false],
1094+
['one authored byte over is red under a prerelease stamp too: the strip buys an author nothing',
1095+
verdict(rel, pricedTokens(stampFixture('18.0.0-next.0', 'x'), { stamped: true }), zeroHeadroom).ok, false],
1096+
['a multi-digit prerelease (-next.12) prices the same as a stable stamp',
1097+
pricedTokens(stampFixture('18.0.0-next.12'), { stamped: true }), zeroHeadroom],
1098+
['a stable stamp prices byte-for-byte as countTokens',
1099+
pricedTokens(stableStamp, { stamped: true }), countTokens(stableStamp)],
1100+
['every real bundle file without a prerelease stamp prices exactly as countTokens', repricedOnStableStamp, []],
1101+
['a prerelease-shaped string outside the stamp is priced',
1102+
pricedTokens(proseStamp, { stamped: true }), countTokens(proseStamp)],
1103+
['a file the version script does not stamp is priced whole',
1104+
pricedTokens(next0Stamp, { stamped: false }), countTokens(next0Stamp)],
1105+
['the stamped set is the version script\'s: exactly the real SKILL.md files',
1106+
[...stampedOnDisk], discovered.filter((p) => /^skills\/[^/]+\/SKILL\.md$/.test(p))],
1107+
9921108
// ── enumeration, never a hand list ───────────────────────────────────
9931109
['the catalog is discovered from disk', discovered.length > 0, true],
9941110
['every discovered file lives under a published skill directory',

0 commit comments

Comments
 (0)