carlos / carloku Public

A figure is a whole number token on both sides, grouping commas included; the check lives in hack/token.mjs with its counterexamples

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev a7283e7a2c5ad6508829f4c2ba3a03f664804bbb parent f291a9e
3 files changed, +23 −4
  • copy-review/pricing/build.mjs +4 −4
  • hack/token.mjs +5 −0
  • test/pricing-pricebook.test.mjs +14 −0
diff --git a/copy-review/pricing/build.mjs b/copy-review/pricing/build.mjs
index 470b5b2..a6e62c4 100644
--- a/copy-review/pricing/build.mjs
+++ b/copy-review/pricing/build.mjs
@@ -5,6 +5,7 @@
// strings.json; never edit the rendered files by hand.
import fs from "node:fs";
import path from "node:path";
+import { carriesToken } from "../../hack/token.mjs";
const here = path.dirname(new URL(import.meta.url).pathname);
const root = path.resolve(here, "..", "..");
const strings = JSON.parse(fs.readFileSync(path.join(here, "strings.json"), "utf8"));
@@ -142,10 +143,9 @@ for (const r of recs) byBand[r.band].push(r.name);
// must checks that an approved string carries a figure as a whole token
// (so "10" is not found inside "100"), not merely as a substring.
const must = (id, text, what) => {
- // A whole token: not followed by a digit, a decimal point, or a
- // grouping comma that continues the number ("10" is not in "10,000").
- const re = new RegExp("(^|[^0-9.])" + text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?![0-9.]|,[0-9])");
- if (!re.test(S[id])) throw new Error(`${id} (${JSON.stringify(S[id])}) does not carry ${what} ${JSON.stringify(text)} from the book`);
+ // A whole token (hack/token.mjs): not part of a larger number on either
+ // side, grouping commas included.
+ if (!carriesToken(S[id], text)) throw new Error(`${id} (${JSON.stringify(S[id])}) does not carry ${what} ${JSON.stringify(text)} from the book`);
};
for (const p of ["community", "business"]) must(`plan.${p}.min`, String(plansByKey[p].min_quantity), "the minimum");
for (const [b, id] of [["B", "bands.b"], ["C", "bands.c"], ["D", "bands.d"]]) must(id, (book.band_factors_bps[b] / 100) + "%", "the band percentage");
diff --git a/hack/token.mjs b/hack/token.mjs
new file mode 100644
index 0000000..4ae1b80
--- /dev/null
+++ b/hack/token.mjs
@@ -0,0 +1,5 @@
+// A figure appears in an approved string as a whole number token: not
+// inside a larger number on either side, a grouping comma included ("10"
+// is in neither "100", "10,000" nor "1,10x"; "250 MB" is not in "1,250 MB").
+export const tokenPattern = (text) => new RegExp("(^|[^0-9.,])" + text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?![0-9.]|,[0-9])");
+export const carriesToken = (haystack, text) => tokenPattern(text).test(haystack);
diff --git a/test/pricing-pricebook.test.mjs b/test/pricing-pricebook.test.mjs
index 3e0ee83..aa8bda2 100644
--- a/test/pricing-pricebook.test.mjs
+++ b/test/pricing-pricebook.test.mjs
@@ -7,6 +7,7 @@ import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import assert from 'node:assert/strict';
import { test } from 'node:test';
+import { carriesToken } from '../hack/token.mjs';
const root = new URL('../', import.meta.url);
const book = JSON.parse(readFileSync(new URL('copy-review/pricing/pricebook.json', root), 'utf8'));
@@ -229,3 +230,16 @@ test('every priced element shows, as static text, exactly its own USD value, and
assert.ok(inBook || inStrings, amount + ' (' + code + ') is on the page and in neither the book nor an approved string');
}
});
+
+test('a figure is carried only as a whole number token, grouping commas on either side included', () => {
+ for (const [text, figure, want] of [
+ ['Minimum 10 members', '10', true],
+ ['Minimum 10,000 members', '10', false],
+ ['Minimum 100 members', '10', false],
+ ['250 MB of storage', '250 MB', true],
+ ['1,250 MB of storage', '250 MB', false],
+ ['1,100 awake hours a month', '100 awake hours', false],
+ ['25%', '25%', true],
+ ['125%', '25%', false],
+ ]) assert.equal(carriesToken(text, figure), want, `${JSON.stringify(figure)} in ${JSON.stringify(text)}`);
+});