@domandigital/craft 0.23.0 → 0.23.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/CHARACTER.md +2 -1
- package/COPY.md +1 -0
- package/dist/{audit-CRTC7JSF.js → audit-SWLTAEDO.js} +3 -3
- package/dist/audit.cjs +72 -3
- package/dist/audit.js +72 -3
- package/dist/{chunk-3LCBLDAO.js → chunk-A5M4AOMY.js} +4 -2
- package/dist/{chunk-Y2LGYHJ5.js → chunk-A5OYA2BG.js} +5 -5
- package/dist/{chunk-KGEJ2TGT.js → chunk-CB4QDMH2.js} +14 -14
- package/dist/{chunk-3LHIKF5L.js → chunk-E2ZYFBQX.js} +5 -5
- package/dist/{chunk-OTQQTXXK.js → chunk-FBGXUVZU.js} +4 -4
- package/dist/{chunk-A7KUKNX6.js → chunk-NK6AAY7O.js} +1 -1
- package/dist/{chunk-4NON4PVK.js → chunk-QNX7CROK.js} +2 -2
- package/dist/{chunk-KIBDFRHT.js → chunk-ZTHVOXZF.js} +69 -2
- package/dist/{cli-TPV2EIIN.js → cli-7IAFZ6CV.js} +9 -9
- package/dist/{cli-X3RZE4GM.js → cli-ATLKYXPU.js} +6 -6
- package/dist/{cli-ROVZDHVR.js → cli-BA3IVSMR.js} +5 -5
- package/dist/{cli-YRRBBU27.js → cli-BP4JLNAU.js} +1 -1
- package/dist/{cli-HWGSNSDD.js → cli-O2IEHB5Q.js} +4 -4
- package/dist/{cli-IYRJZWMN.js → cli-T5L55IWF.js} +6 -6
- package/dist/{cli-RHU3MAZY.js → cli-UPO4PR3Z.js} +3 -3
- package/dist/{cli-E2CFVIGZ.js → cli-ZMLKL2NT.js} +5 -5
- package/dist/cli.js +3 -3
- package/dist/index.cjs +72 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +72 -3
- package/dist/{viewports-cli-EAUVLHFJ.js → viewports-cli-Y4HPKZLI.js} +3 -3
- package/package.json +1 -1
- package/word-lists.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @domandigital/craft
|
|
2
2
|
|
|
3
|
+
## 0.23.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1e4bcb5: <!-- copy-ok: names the shapes the release starts reporting --> A new review-tier copy tell, `absolute-negation` (catalogue 2026.10.9), reports a verbless fragment that opens on never, nothing, nobody or "no" + noun after a clause break: "A guardian consents by email; never pre-ticked", "Pay per user per month; no servers", "nothing to buy or run", "Nothing on-site to join", "no duplicates". A 92EL deck passed the gate with them. It never blocks. A sentence with a subject and a verb ("Players never open the lists", "Nothing is copied by hand"), an instruction ("Never share your password"), a Harvard citation with no date ("ICO, no date") and a "No" form answer pass, and a fragment the blocking `no-x-no-y`, `no-x-badge` or `negative-reassurance` already reports is not reported twice.
|
|
8
|
+
|
|
9
|
+
## 0.23.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 297ee1b: `trailing-contrast` no longer goes quiet on a sentence that follows one `not-just-but` blocks (DOM-1358, catalogue 2026.10.8). The check that keeps one sentence from being reported twice looked back a fixed 80 characters and could reach the sentence before; it now reads only the sentence the match sits in. Found by running the released `copy-check` over the 92EL lines: "Edge matters for game servers, not registration." went unreported when it came straight after the gerund pair.
|
|
14
|
+
|
|
3
15
|
## 0.23.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/CHARACTER.md
CHANGED
|
@@ -1470,7 +1470,7 @@ Generated from the package. Run `pnpm --filter @domandigital/craft run docs`
|
|
|
1470
1470
|
after changing an entry; a test fails until you do.
|
|
1471
1471
|
|
|
1472
1472
|
<!-- craft:catalogue:start -->
|
|
1473
|
-
Catalogue version `2026.10.
|
|
1473
|
+
Catalogue version `2026.10.9`, 77 tells.
|
|
1474
1474
|
|
|
1475
1475
|
| Id | Gen | Surface | Severity | Tell | Why it is a default |
|
|
1476
1476
|
| --- | --- | --- | --- | --- | --- |
|
|
@@ -1520,6 +1520,7 @@ Catalogue version `2026.10.7`, 76 tells.
|
|
|
1520
1520
|
| `no-x-badge` | 2 | copy | warn | 'No X' badge | 'No catch.', 'NO JARGON GUIDE': a short standalone 'No X' reads as a slapped-on kicker, and worse when the same one is reused across pieces. |
|
|
1521
1521
|
| `staccato-triplet` | 2 | copy | warn | Staccato triplet | Three fragments in a row ('Fast. Friendly. Local.' or 'No fuss. No jargon. Just results.') (a tricolon in asyndeton: see COPY.md) is the second wave's favourite rhythm. |
|
|
1522
1522
|
| `parallel-pair` | 2 | copy | warn | Verbless parallel pair | 'One form, seven workflows', 'Same site, same £8,000': two matched fragments with no verb (isocolon, often with anaphora or asyndeton: see COPY.md), the staccato triplet's two-part cousin. It sounds decided and says less than a sentence would. |
|
|
1523
|
+
| `absolute-negation` | 2 | copy | warn | Absolute negation fragment | 'never pre-ticked', 'no servers', 'nothing to buy or run': a verbless fragment that opens on never, nothing, nobody or 'no' + noun. Read aloud it sounds rigid, it promises an absolute the copy rarely means, and it defines the thing by what it is not. |
|
|
1523
1524
|
| `where-x-meets-y` | 2 | copy | warn | 'Where X meets Y' | 'Where luxury meets comfort' is a tagline shape that fits every business and so describes none. |
|
|
1524
1525
|
| `chatbot-residue` | 1 | copy | warn | Chatbot residue | 'Certainly! Here's a revised version', 'as an AI language model', a citeturn0search0 token, a link tracked utm_source=chatgpt.com: text that exists only because a chat reply was pasted whole. This is evidence, not style. |
|
|
1525
1526
|
| `stock-email-line` | 1 | copy | warn | Stock email line | 'I hope this email finds you well', 'let me know if you have any questions', 'don't hesitate to contact us': the opener and closer of a form email. A client reads past them, and a model drafting an email writes them every time. |
|
package/COPY.md
CHANGED
|
@@ -101,6 +101,7 @@ person reading decides which instance earns its place.
|
|
|
101
101
|
| `matey-register` | the put-on British voice: "Alright Dave,", "cheers mate", "no worries", "we'll get you sorted", "proper good", "give me a shout", "chuffed", "Right then,"; and clipped speech that drops the small words: "have look at", "give it go", "Quick one:", "built you a site". Write plain, complete British English |
|
|
102
102
|
| `confession-line` | scoping the business by what it has not done, will not do or is not: "What this is and is not", "we have not yet built", "our clinic experience so far is one practice", "what we would and wouldn't take on", "who we don't work with". It's an open shop: be honest by being precise ("the portal we run for Sensphere") and keep the door open. Contractual facts (eligibility, who runs what) stay, said as what the buyer gets |
|
|
103
103
|
| `trailing-contrast` | the softer contrast template: a noun or phrase after ", not" closing a sentence ("Edge matters for game servers, not registration"), "not a legal one", and "renting computing instead of owning it". A genuine disclaimer has the same shape ("general information, not a recommendation"), so it is reported, never blocked. Say the positive claim; keep the contrast only where the reader would otherwise assume the other thing |
|
|
104
|
+
| `absolute-negation` | a verbless fragment that opens on never, nothing, nobody or "no" + noun, after a clause break: "A guardian consents by email; never pre-ticked", "Pay per user per month; no servers", "nothing to buy or run", "Nothing on-site to join", "no duplicates". Read aloud it sounds rigid and promises an absolute. A sentence with a subject and a verb ("Players never open the lists", "Nothing is copied by hand"), an instruction ("Never share your password"), a citation with no date ("ICO, no date") and a "No" form answer pass. Say what happens as a sentence; keep the absolute only where it is literally true and the reader needs it |
|
|
104
105
|
| `outcome-promise` | a hoped-for result written as delivered: "we'll turn that into enquiries", "once the first areas are working", "competitors you can outdo", "exactly what convinces", "guaranteed rankings", "you'll get more". Promise the work; measure and report the results |
|
|
105
106
|
| `ai-vocabulary` | tapestry, testament, realm, elevate, nestled, meticulous, unparalleled, moreover; and underscore, showcasing, pivotal, measured 10 to 14 times over-used in post-2022 text |
|
|
106
107
|
| `stock-email-line` | the form-email opener and closer: "I hope this email finds you well", "hope you're having a great week", "let me know if you have any questions", "don't hesitate to contact us". Start with the reason for writing; end with the next step and who to ask |
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
snapshotPage,
|
|
10
10
|
snapshotUrl,
|
|
11
11
|
snapshotUrls
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-NK6AAY7O.js";
|
|
13
13
|
import "./chunk-JYNXGCYZ.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-A5M4AOMY.js";
|
|
15
|
+
import "./chunk-ZTHVOXZF.js";
|
|
16
16
|
import "./chunk-QYBAVU3P.js";
|
|
17
17
|
export {
|
|
18
18
|
PROVENANCE_BUDGET_MS,
|
package/dist/audit.cjs
CHANGED
|
@@ -358,8 +358,33 @@ var PARALLEL_PAIR = [
|
|
|
358
358
|
// a verbless list as a tagline: "Service account, match-day reminders, analytics."
|
|
359
359
|
/^[ \t]*(?:#{1,6}[ \t]+)?[A-Z][\w'’-]*(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}(?:,[ \t]+(?!(?:and|or)\b)[\w'’-]+(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}){2,3}\.[ \t]*$/m
|
|
360
360
|
];
|
|
361
|
+
var CLAUSE_START = String.raw`(?<=^[ \t]*(?:[-*][ \t]+|#{1,6}[ \t]+)?|[;:,(—–][ \t]*|[.!?][ \t]+)`;
|
|
362
|
+
var ABSOLUTE_NEGATION = new RegExp(
|
|
363
|
+
`${CLAUSE_START}(?:never|nothing|nobody|no[ \\t]+one|no)(?:[ \\t]+[\\w'\u2019\xA3$\u20AC-]+){1,6}(?=[ \\t]*(?:[.,;:!?)"\u201D\u2014\u2013]|$))`,
|
|
364
|
+
"im"
|
|
365
|
+
);
|
|
366
|
+
var FINITE = /^(?:is|are|was|were|am|be|been|being|has|have|had|do|does|did|can|could|will|would|shall|should|may|might|must|need|needs|gets|got|goes|went|comes|came|makes|means|matters|happens|stays|changes|works|takes|says|knows|seems|feels|looks|leaves|left|appears|uses|runs|shows|sends|holds|reads|writes|opens|stores|reaches|applies|isn['’]t|aren['’]t|wasn['’]t)$/i;
|
|
367
|
+
var NO_IDIOM = /^(?:date|longer|later|sooner|matter|more|less|fewer|other|one|doubt|wonder|thanks|problem)$/i;
|
|
368
|
+
var PARTICIPLE = /^[\w-]*(?:ed|en|wn|ld|lt|nt|pt|ne|id)$/i;
|
|
369
|
+
function notAbsoluteFragment(block, match, index) {
|
|
370
|
+
const [head, ...rest] = match.trim().split(/[ \t]+/);
|
|
371
|
+
let words = rest;
|
|
372
|
+
const lead = head.toLowerCase();
|
|
373
|
+
if (lead === "no" && words[0]?.toLowerCase() === "one") words = words.slice(1);
|
|
374
|
+
else if (lead === "no" && NO_IDIOM.test(words[0] ?? "")) return true;
|
|
375
|
+
if (words.some((w) => FINITE.test(w) || /['’](?:s|re|ve|ll|d)$/i.test(w))) return true;
|
|
376
|
+
if (lead === "never" && !PARTICIPLE.test(words[0] ?? "")) return true;
|
|
377
|
+
if (lead !== "no" && lead !== "never" && /^(?!else$|less$|across$)[a-z]+s$/i.test(words[0] ?? "")) return true;
|
|
378
|
+
const sentence = lineAround(block.text, index);
|
|
379
|
+
const line = sentence.trim().replace(/^[-*]\s+/, "");
|
|
380
|
+
if (NO_X_NO_Y.test(sentence) || NO_X_BADGE.test(line)) return true;
|
|
381
|
+
if (NEGATIVE_REASSURANCE.some((p) => wordList([p]).test(match))) return true;
|
|
382
|
+
return emptyState(block, match, index);
|
|
383
|
+
}
|
|
361
384
|
var alreadyBlocked = (block, match, index) => {
|
|
362
|
-
const
|
|
385
|
+
const before = block.text.slice(0, index);
|
|
386
|
+
const start = Math.max(before.search(/[^.!?\n]*$/), 0);
|
|
387
|
+
const sentence = block.text.slice(start, index + match.length);
|
|
363
388
|
return NEGATION.some((pattern) => pattern.test(sentence));
|
|
364
389
|
};
|
|
365
390
|
var DATA_ABSENCE = /\b(?:yet|found|available|configured|selected|assigned|scheduled|recorded|on file|to show|to display|results|match|matches|matching|data|in this period)\b/i;
|
|
@@ -788,7 +813,9 @@ var COPY_TELLS = [
|
|
|
788
813
|
f("content/report.md", "Edge matters for game servers, not registration."),
|
|
789
814
|
f("content/report.md", "Age 18 is the league's own threshold, not a legal one."),
|
|
790
815
|
f("content/report.md", "Cloud computing means renting computing over the internet instead of owning it."),
|
|
791
|
-
f("content/home.md", "Real Square data, not a vanity metric.")
|
|
816
|
+
f("content/home.md", "Real Square data, not a vanity metric."),
|
|
817
|
+
// Reported even straight after a sentence the blocking rule reports.
|
|
818
|
+
f("content/report.md", "That means renting services, not running servers.\n\nEdge matters for game servers, not registration.")
|
|
792
819
|
],
|
|
793
820
|
pass: [
|
|
794
821
|
f("content/home.md", "Rewire, retile and replumb a bathroom in five days."),
|
|
@@ -892,6 +919,46 @@ var COPY_TELLS = [
|
|
|
892
919
|
]
|
|
893
920
|
}
|
|
894
921
|
},
|
|
922
|
+
{
|
|
923
|
+
id: "absolute-negation",
|
|
924
|
+
name: "Absolute negation fragment",
|
|
925
|
+
generation: 2,
|
|
926
|
+
severity: "warn",
|
|
927
|
+
surface: "copy",
|
|
928
|
+
why: "'never pre-ticked', 'no servers', 'nothing to buy or run': a verbless fragment that opens on never, nothing, nobody or 'no' + noun. Read aloud it sounds rigid, it promises an absolute the copy rarely means, and it defines the thing by what it is not.",
|
|
929
|
+
fix: "Say what happens, as a sentence: 'A guardian ticks the consent box in the email.', 'Microsoft runs the servers.' Keep the absolute only where it is literally true and the reader needs it, and give it a subject and a verb.",
|
|
930
|
+
detect: phrases([ABSOLUTE_NEGATION], quoted, allText, notAbsoluteFragment),
|
|
931
|
+
fixtures: {
|
|
932
|
+
flag: [
|
|
933
|
+
// From a 92EL deck that passed the gate, October 2026.
|
|
934
|
+
f("content/deck.md", "A guardian consents by email; never pre-ticked."),
|
|
935
|
+
f("content/deck.md", "Pay per user per month; no servers."),
|
|
936
|
+
f("content/deck.md", "Microsoft runs it, so there is less to manage: nothing to buy or run."),
|
|
937
|
+
f("content/deck.md", "Players register on their phone. Nothing on-site to join."),
|
|
938
|
+
f("content/deck.md", "Each player is checked against the list once, no duplicates."),
|
|
939
|
+
f("app/slide.tsx", `export const note = "Nobody to chase for a form.";`)
|
|
940
|
+
],
|
|
941
|
+
pass: [
|
|
942
|
+
f("content/report.md", "Players never open the lists."),
|
|
943
|
+
f("content/report.md", "Nothing is copied by hand."),
|
|
944
|
+
f("content/report.md", "It is not tested at national scale."),
|
|
945
|
+
f("content/report.md", "Never share your password with anyone."),
|
|
946
|
+
f("content/report.md", "No one knows the password but the club secretary."),
|
|
947
|
+
// A Harvard citation with no date.
|
|
948
|
+
f("content/report.md", "Children's data needs a lawful basis (ICO, no date)."),
|
|
949
|
+
f("content/report.md", "- Information Commissioner's Office (ICO), no date. Children and the UK GDPR."),
|
|
950
|
+
// A form answer option.
|
|
951
|
+
f("content/form.md", "Is the player under 18?\n\n- Yes\n- No"),
|
|
952
|
+
f("app/form.tsx", `export const options = ["Yes", "No"];`),
|
|
953
|
+
f("content/form.md", "No, the player is 18 or over."),
|
|
954
|
+
f("content/home.md", "We no longer post paper forms."),
|
|
955
|
+
f("content/report.md", "If the check fails, nothing new appears in the list."),
|
|
956
|
+
// The blocking tells report these.
|
|
957
|
+
f("content/home.md", "We reply within a day. No obligation, no spam."),
|
|
958
|
+
f("app/list.tsx", `export const empty = "No results found.";`)
|
|
959
|
+
]
|
|
960
|
+
}
|
|
961
|
+
},
|
|
895
962
|
{
|
|
896
963
|
id: "where-x-meets-y",
|
|
897
964
|
name: "'Where X meets Y'",
|
|
@@ -3495,6 +3562,8 @@ var HOUSE = {
|
|
|
3495
3562
|
// New 2026-10-23 (founder, from 92EL deliverables): review, since a short slide label is often fine.
|
|
3496
3563
|
"parallel-pair": { tier: "review", label: "verbless parallel pair, write the sentence it stands for" },
|
|
3497
3564
|
"trailing-contrast": { tier: "review", label: "trailing contrast. Say the positive claim, unless it is a disclaimer the reader needs" },
|
|
3565
|
+
// New 2026-10-26 (founder, from a 92EL deck): review first, like every new tell, until estate hits are read.
|
|
3566
|
+
"absolute-negation": { tier: "review", label: "absolute negation fragment. Say what happens, as a sentence with a subject and a verb" },
|
|
3498
3567
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
3499
3568
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
3500
3569
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -4263,7 +4332,7 @@ var IMAGERY_TELLS = [
|
|
|
4263
4332
|
];
|
|
4264
4333
|
|
|
4265
4334
|
// src/character/check.ts
|
|
4266
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
4335
|
+
var CATALOGUE_VERSION = "2026.10.9";
|
|
4267
4336
|
var CATALOGUE = [
|
|
4268
4337
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
4269
4338
|
...SPECIFICITY_COPY_TELLS,
|
package/dist/audit.js
CHANGED
|
@@ -323,8 +323,33 @@ var PARALLEL_PAIR = [
|
|
|
323
323
|
// a verbless list as a tagline: "Service account, match-day reminders, analytics."
|
|
324
324
|
/^[ \t]*(?:#{1,6}[ \t]+)?[A-Z][\w'’-]*(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}(?:,[ \t]+(?!(?:and|or)\b)[\w'’-]+(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}){2,3}\.[ \t]*$/m
|
|
325
325
|
];
|
|
326
|
+
var CLAUSE_START = String.raw`(?<=^[ \t]*(?:[-*][ \t]+|#{1,6}[ \t]+)?|[;:,(—–][ \t]*|[.!?][ \t]+)`;
|
|
327
|
+
var ABSOLUTE_NEGATION = new RegExp(
|
|
328
|
+
`${CLAUSE_START}(?:never|nothing|nobody|no[ \\t]+one|no)(?:[ \\t]+[\\w'\u2019\xA3$\u20AC-]+){1,6}(?=[ \\t]*(?:[.,;:!?)"\u201D\u2014\u2013]|$))`,
|
|
329
|
+
"im"
|
|
330
|
+
);
|
|
331
|
+
var FINITE = /^(?:is|are|was|were|am|be|been|being|has|have|had|do|does|did|can|could|will|would|shall|should|may|might|must|need|needs|gets|got|goes|went|comes|came|makes|means|matters|happens|stays|changes|works|takes|says|knows|seems|feels|looks|leaves|left|appears|uses|runs|shows|sends|holds|reads|writes|opens|stores|reaches|applies|isn['’]t|aren['’]t|wasn['’]t)$/i;
|
|
332
|
+
var NO_IDIOM = /^(?:date|longer|later|sooner|matter|more|less|fewer|other|one|doubt|wonder|thanks|problem)$/i;
|
|
333
|
+
var PARTICIPLE = /^[\w-]*(?:ed|en|wn|ld|lt|nt|pt|ne|id)$/i;
|
|
334
|
+
function notAbsoluteFragment(block, match, index) {
|
|
335
|
+
const [head, ...rest] = match.trim().split(/[ \t]+/);
|
|
336
|
+
let words = rest;
|
|
337
|
+
const lead = head.toLowerCase();
|
|
338
|
+
if (lead === "no" && words[0]?.toLowerCase() === "one") words = words.slice(1);
|
|
339
|
+
else if (lead === "no" && NO_IDIOM.test(words[0] ?? "")) return true;
|
|
340
|
+
if (words.some((w) => FINITE.test(w) || /['’](?:s|re|ve|ll|d)$/i.test(w))) return true;
|
|
341
|
+
if (lead === "never" && !PARTICIPLE.test(words[0] ?? "")) return true;
|
|
342
|
+
if (lead !== "no" && lead !== "never" && /^(?!else$|less$|across$)[a-z]+s$/i.test(words[0] ?? "")) return true;
|
|
343
|
+
const sentence = lineAround(block.text, index);
|
|
344
|
+
const line = sentence.trim().replace(/^[-*]\s+/, "");
|
|
345
|
+
if (NO_X_NO_Y.test(sentence) || NO_X_BADGE.test(line)) return true;
|
|
346
|
+
if (NEGATIVE_REASSURANCE.some((p) => wordList([p]).test(match))) return true;
|
|
347
|
+
return emptyState(block, match, index);
|
|
348
|
+
}
|
|
326
349
|
var alreadyBlocked = (block, match, index) => {
|
|
327
|
-
const
|
|
350
|
+
const before = block.text.slice(0, index);
|
|
351
|
+
const start = Math.max(before.search(/[^.!?\n]*$/), 0);
|
|
352
|
+
const sentence = block.text.slice(start, index + match.length);
|
|
328
353
|
return NEGATION.some((pattern) => pattern.test(sentence));
|
|
329
354
|
};
|
|
330
355
|
var DATA_ABSENCE = /\b(?:yet|found|available|configured|selected|assigned|scheduled|recorded|on file|to show|to display|results|match|matches|matching|data|in this period)\b/i;
|
|
@@ -753,7 +778,9 @@ var COPY_TELLS = [
|
|
|
753
778
|
f("content/report.md", "Edge matters for game servers, not registration."),
|
|
754
779
|
f("content/report.md", "Age 18 is the league's own threshold, not a legal one."),
|
|
755
780
|
f("content/report.md", "Cloud computing means renting computing over the internet instead of owning it."),
|
|
756
|
-
f("content/home.md", "Real Square data, not a vanity metric.")
|
|
781
|
+
f("content/home.md", "Real Square data, not a vanity metric."),
|
|
782
|
+
// Reported even straight after a sentence the blocking rule reports.
|
|
783
|
+
f("content/report.md", "That means renting services, not running servers.\n\nEdge matters for game servers, not registration.")
|
|
757
784
|
],
|
|
758
785
|
pass: [
|
|
759
786
|
f("content/home.md", "Rewire, retile and replumb a bathroom in five days."),
|
|
@@ -857,6 +884,46 @@ var COPY_TELLS = [
|
|
|
857
884
|
]
|
|
858
885
|
}
|
|
859
886
|
},
|
|
887
|
+
{
|
|
888
|
+
id: "absolute-negation",
|
|
889
|
+
name: "Absolute negation fragment",
|
|
890
|
+
generation: 2,
|
|
891
|
+
severity: "warn",
|
|
892
|
+
surface: "copy",
|
|
893
|
+
why: "'never pre-ticked', 'no servers', 'nothing to buy or run': a verbless fragment that opens on never, nothing, nobody or 'no' + noun. Read aloud it sounds rigid, it promises an absolute the copy rarely means, and it defines the thing by what it is not.",
|
|
894
|
+
fix: "Say what happens, as a sentence: 'A guardian ticks the consent box in the email.', 'Microsoft runs the servers.' Keep the absolute only where it is literally true and the reader needs it, and give it a subject and a verb.",
|
|
895
|
+
detect: phrases([ABSOLUTE_NEGATION], quoted, allText, notAbsoluteFragment),
|
|
896
|
+
fixtures: {
|
|
897
|
+
flag: [
|
|
898
|
+
// From a 92EL deck that passed the gate, October 2026.
|
|
899
|
+
f("content/deck.md", "A guardian consents by email; never pre-ticked."),
|
|
900
|
+
f("content/deck.md", "Pay per user per month; no servers."),
|
|
901
|
+
f("content/deck.md", "Microsoft runs it, so there is less to manage: nothing to buy or run."),
|
|
902
|
+
f("content/deck.md", "Players register on their phone. Nothing on-site to join."),
|
|
903
|
+
f("content/deck.md", "Each player is checked against the list once, no duplicates."),
|
|
904
|
+
f("app/slide.tsx", `export const note = "Nobody to chase for a form.";`)
|
|
905
|
+
],
|
|
906
|
+
pass: [
|
|
907
|
+
f("content/report.md", "Players never open the lists."),
|
|
908
|
+
f("content/report.md", "Nothing is copied by hand."),
|
|
909
|
+
f("content/report.md", "It is not tested at national scale."),
|
|
910
|
+
f("content/report.md", "Never share your password with anyone."),
|
|
911
|
+
f("content/report.md", "No one knows the password but the club secretary."),
|
|
912
|
+
// A Harvard citation with no date.
|
|
913
|
+
f("content/report.md", "Children's data needs a lawful basis (ICO, no date)."),
|
|
914
|
+
f("content/report.md", "- Information Commissioner's Office (ICO), no date. Children and the UK GDPR."),
|
|
915
|
+
// A form answer option.
|
|
916
|
+
f("content/form.md", "Is the player under 18?\n\n- Yes\n- No"),
|
|
917
|
+
f("app/form.tsx", `export const options = ["Yes", "No"];`),
|
|
918
|
+
f("content/form.md", "No, the player is 18 or over."),
|
|
919
|
+
f("content/home.md", "We no longer post paper forms."),
|
|
920
|
+
f("content/report.md", "If the check fails, nothing new appears in the list."),
|
|
921
|
+
// The blocking tells report these.
|
|
922
|
+
f("content/home.md", "We reply within a day. No obligation, no spam."),
|
|
923
|
+
f("app/list.tsx", `export const empty = "No results found.";`)
|
|
924
|
+
]
|
|
925
|
+
}
|
|
926
|
+
},
|
|
860
927
|
{
|
|
861
928
|
id: "where-x-meets-y",
|
|
862
929
|
name: "'Where X meets Y'",
|
|
@@ -3460,6 +3527,8 @@ var HOUSE = {
|
|
|
3460
3527
|
// New 2026-10-23 (founder, from 92EL deliverables): review, since a short slide label is often fine.
|
|
3461
3528
|
"parallel-pair": { tier: "review", label: "verbless parallel pair, write the sentence it stands for" },
|
|
3462
3529
|
"trailing-contrast": { tier: "review", label: "trailing contrast. Say the positive claim, unless it is a disclaimer the reader needs" },
|
|
3530
|
+
// New 2026-10-26 (founder, from a 92EL deck): review first, like every new tell, until estate hits are read.
|
|
3531
|
+
"absolute-negation": { tier: "review", label: "absolute negation fragment. Say what happens, as a sentence with a subject and a verb" },
|
|
3463
3532
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
3464
3533
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
3465
3534
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -4228,7 +4297,7 @@ var IMAGERY_TELLS = [
|
|
|
4228
4297
|
];
|
|
4229
4298
|
|
|
4230
4299
|
// src/character/check.ts
|
|
4231
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
4300
|
+
var CATALOGUE_VERSION = "2026.10.9";
|
|
4232
4301
|
var CATALOGUE = [
|
|
4233
4302
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
4234
4303
|
...SPECIFICITY_COPY_TELLS,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
parseFile,
|
|
10
10
|
sentencesOf,
|
|
11
11
|
specificity
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-ZTHVOXZF.js";
|
|
13
13
|
import {
|
|
14
14
|
SNAPSHOT_VERSION
|
|
15
15
|
} from "./chunk-QYBAVU3P.js";
|
|
@@ -2573,6 +2573,8 @@ var HOUSE = {
|
|
|
2573
2573
|
// New 2026-10-23 (founder, from 92EL deliverables): review, since a short slide label is often fine.
|
|
2574
2574
|
"parallel-pair": { tier: "review", label: "verbless parallel pair, write the sentence it stands for" },
|
|
2575
2575
|
"trailing-contrast": { tier: "review", label: "trailing contrast. Say the positive claim, unless it is a disclaimer the reader needs" },
|
|
2576
|
+
// New 2026-10-26 (founder, from a 92EL deck): review first, like every new tell, until estate hits are read.
|
|
2577
|
+
"absolute-negation": { tier: "review", label: "absolute negation fragment. Say what happens, as a sentence with a subject and a verb" },
|
|
2576
2578
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
2577
2579
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
2578
2580
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -3195,7 +3197,7 @@ var IMAGERY_TELLS = [
|
|
|
3195
3197
|
];
|
|
3196
3198
|
|
|
3197
3199
|
// src/character/check.ts
|
|
3198
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
3200
|
+
var CATALOGUE_VERSION = "2026.10.9";
|
|
3199
3201
|
var CATALOGUE = [
|
|
3200
3202
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
3201
3203
|
...SPECIFICITY_COPY_TELLS,
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
formatDirectionMatches,
|
|
5
5
|
summariseDirection,
|
|
6
6
|
validateDirection
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QNX7CROK.js";
|
|
8
8
|
import {
|
|
9
9
|
loadNull
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-E2ZYFBQX.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-CB4QDMH2.js";
|
|
14
14
|
import {
|
|
15
15
|
toJson
|
|
16
16
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
rhythmOf,
|
|
31
31
|
shapeName,
|
|
32
32
|
temperatureName
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-A5M4AOMY.js";
|
|
34
34
|
|
|
35
35
|
// src/estate/cli.ts
|
|
36
36
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -133,7 +133,7 @@ async function snapshotOf(target, cwd) {
|
|
|
133
133
|
return snap;
|
|
134
134
|
}
|
|
135
135
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
|
|
136
|
-
const { snapshotUrl } = await import("./audit-
|
|
136
|
+
const { snapshotUrl } = await import("./audit-SWLTAEDO.js");
|
|
137
137
|
return snapshotUrl(target);
|
|
138
138
|
}
|
|
139
139
|
var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
holdHouseBlocks,
|
|
11
11
|
houseRule,
|
|
12
12
|
scanSource
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-A5M4AOMY.js";
|
|
14
14
|
import {
|
|
15
15
|
compareCompetitor,
|
|
16
16
|
compareFacts,
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
pageCopy,
|
|
22
22
|
protectedFacts,
|
|
23
23
|
visibleText
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-ZTHVOXZF.js";
|
|
25
25
|
|
|
26
26
|
// src/character/config.ts
|
|
27
27
|
var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
|
|
@@ -622,7 +622,7 @@ async function pageSnapshot(target, cwd) {
|
|
|
622
622
|
return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
|
|
623
623
|
}
|
|
624
624
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
|
|
625
|
-
const { snapshotUrl } = await import("./audit-
|
|
625
|
+
const { snapshotUrl } = await import("./audit-SWLTAEDO.js");
|
|
626
626
|
return snapshotUrl(target);
|
|
627
627
|
}
|
|
628
628
|
async function briefFor(flags, cwd) {
|
|
@@ -633,7 +633,7 @@ async function briefFor(flags, cwd) {
|
|
|
633
633
|
if (typeof brief === "string" && brief.trim()) return brief;
|
|
634
634
|
} else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
|
|
635
635
|
if (flags.null) {
|
|
636
|
-
const { loadNull } = await import("./cli-
|
|
636
|
+
const { loadNull } = await import("./cli-O2IEHB5Q.js");
|
|
637
637
|
return loadNull(flags.null, cwd).brief;
|
|
638
638
|
}
|
|
639
639
|
return void 0;
|
|
@@ -659,37 +659,37 @@ async function compareWithCompetitor(flags, io) {
|
|
|
659
659
|
function run(argv, io) {
|
|
660
660
|
const [command, ...rest] = argv;
|
|
661
661
|
if (command === "direction") {
|
|
662
|
-
return import("./cli-
|
|
662
|
+
return import("./cli-ZMLKL2NT.js").then((m) => m.runDirection(rest, io));
|
|
663
663
|
}
|
|
664
664
|
if (command === "report" || command === "retrofit") {
|
|
665
|
-
return import("./cli-
|
|
665
|
+
return import("./cli-7IAFZ6CV.js").then((m) => m.runReport(command, rest, io));
|
|
666
666
|
}
|
|
667
667
|
if (command === "register") {
|
|
668
|
-
return import("./cli-
|
|
668
|
+
return import("./cli-BP4JLNAU.js").then((m) => m.runRegister(rest, io));
|
|
669
669
|
}
|
|
670
670
|
if (command === "estate") {
|
|
671
|
-
return import("./cli-
|
|
671
|
+
return import("./cli-ATLKYXPU.js").then((m) => m.runEstate(rest, io));
|
|
672
672
|
}
|
|
673
673
|
if (command === "null") {
|
|
674
|
-
return import("./cli-
|
|
674
|
+
return import("./cli-O2IEHB5Q.js").then((m) => m.runNull(rest, io));
|
|
675
675
|
}
|
|
676
676
|
if (command === "calibrate") {
|
|
677
|
-
return import("./cli-
|
|
677
|
+
return import("./cli-BA3IVSMR.js").then((m) => m.runCalibrate(rest, io));
|
|
678
678
|
}
|
|
679
679
|
if (command === "tells" && rest[0] === "harvest") {
|
|
680
|
-
return import("./cli-
|
|
680
|
+
return import("./cli-O2IEHB5Q.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
|
|
681
681
|
io.err(`craft: ${error.message}`);
|
|
682
682
|
return 2;
|
|
683
683
|
});
|
|
684
684
|
}
|
|
685
685
|
if (command === "phone-first") {
|
|
686
|
-
return import("./cli-
|
|
686
|
+
return import("./cli-UPO4PR3Z.js").then((m) => m.runPhoneFirst(rest, io));
|
|
687
687
|
}
|
|
688
688
|
if (command === "viewports") {
|
|
689
|
-
return import("./viewports-cli-
|
|
689
|
+
return import("./viewports-cli-Y4HPKZLI.js").then((m) => m.runViewports(rest, io));
|
|
690
690
|
}
|
|
691
691
|
if (command === "snapshot" || command === "audit") {
|
|
692
|
-
return import("./cli-
|
|
692
|
+
return import("./cli-T5L55IWF.js").then((m) => m.runAudit(command, rest, io));
|
|
693
693
|
}
|
|
694
694
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
695
695
|
io.out(HELP);
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-OQXTQQAU.js";
|
|
4
4
|
import {
|
|
5
5
|
parseFlags
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-CB4QDMH2.js";
|
|
7
7
|
import {
|
|
8
8
|
toJson
|
|
9
9
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -22,10 +22,10 @@ import {
|
|
|
22
22
|
hueSwatch,
|
|
23
23
|
nullPrompt,
|
|
24
24
|
scanSource
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-A5M4AOMY.js";
|
|
26
26
|
import {
|
|
27
27
|
extractStrings
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-ZTHVOXZF.js";
|
|
29
29
|
|
|
30
30
|
// src/null/cli.ts
|
|
31
31
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
@@ -120,7 +120,7 @@ async function build(flags, io) {
|
|
|
120
120
|
const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
|
|
121
121
|
if (toShoot.length) {
|
|
122
122
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
|
|
123
|
-
const { snapshotUrls } = await import("./audit-
|
|
123
|
+
const { snapshotUrls } = await import("./audit-SWLTAEDO.js");
|
|
124
124
|
const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
|
|
125
125
|
results.forEach((r, i) => {
|
|
126
126
|
const id = toShoot[i];
|
|
@@ -241,7 +241,7 @@ async function importPages(flags, io) {
|
|
|
241
241
|
const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
|
|
242
242
|
if (toShoot.length) {
|
|
243
243
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
|
|
244
|
-
const { snapshotUrls } = await import("./audit-
|
|
244
|
+
const { snapshotUrls } = await import("./audit-SWLTAEDO.js");
|
|
245
245
|
const servers = /* @__PURE__ */ new Map();
|
|
246
246
|
try {
|
|
247
247
|
for (const p of toShoot) if (!servers.has(p.root)) servers.set(p.root, await serveDir(p.root));
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
snapshotUrl,
|
|
3
3
|
snapshotUrls
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NK6AAY7O.js";
|
|
5
5
|
import {
|
|
6
6
|
describeTypicality,
|
|
7
7
|
loadNull
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-E2ZYFBQX.js";
|
|
9
9
|
import {
|
|
10
10
|
SOURCE_FILE,
|
|
11
11
|
finish,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
parseFlags,
|
|
15
15
|
prepareReport,
|
|
16
16
|
readPaths
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-CB4QDMH2.js";
|
|
18
18
|
import {
|
|
19
19
|
toJson
|
|
20
20
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
rhythmOf,
|
|
29
29
|
scanSource,
|
|
30
30
|
typicality
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-A5M4AOMY.js";
|
|
32
32
|
|
|
33
33
|
// src/audit/cli.ts
|
|
34
34
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
levenshtein,
|
|
11
11
|
normaliseFamily,
|
|
12
12
|
parseColour
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-A5M4AOMY.js";
|
|
14
14
|
import {
|
|
15
15
|
TRADE_NOUNS
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-ZTHVOXZF.js";
|
|
17
17
|
|
|
18
18
|
// src/direction/types.ts
|
|
19
19
|
var DIRECTION_VERSION = 2;
|
|
@@ -746,8 +746,33 @@ var PARALLEL_PAIR = [
|
|
|
746
746
|
// a verbless list as a tagline: "Service account, match-day reminders, analytics."
|
|
747
747
|
/^[ \t]*(?:#{1,6}[ \t]+)?[A-Z][\w'’-]*(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}(?:,[ \t]+(?!(?:and|or)\b)[\w'’-]+(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}){2,3}\.[ \t]*$/m
|
|
748
748
|
];
|
|
749
|
+
var CLAUSE_START = String.raw`(?<=^[ \t]*(?:[-*][ \t]+|#{1,6}[ \t]+)?|[;:,(—–][ \t]*|[.!?][ \t]+)`;
|
|
750
|
+
var ABSOLUTE_NEGATION = new RegExp(
|
|
751
|
+
`${CLAUSE_START}(?:never|nothing|nobody|no[ \\t]+one|no)(?:[ \\t]+[\\w'\u2019\xA3$\u20AC-]+){1,6}(?=[ \\t]*(?:[.,;:!?)"\u201D\u2014\u2013]|$))`,
|
|
752
|
+
"im"
|
|
753
|
+
);
|
|
754
|
+
var FINITE = /^(?:is|are|was|were|am|be|been|being|has|have|had|do|does|did|can|could|will|would|shall|should|may|might|must|need|needs|gets|got|goes|went|comes|came|makes|means|matters|happens|stays|changes|works|takes|says|knows|seems|feels|looks|leaves|left|appears|uses|runs|shows|sends|holds|reads|writes|opens|stores|reaches|applies|isn['’]t|aren['’]t|wasn['’]t)$/i;
|
|
755
|
+
var NO_IDIOM = /^(?:date|longer|later|sooner|matter|more|less|fewer|other|one|doubt|wonder|thanks|problem)$/i;
|
|
756
|
+
var PARTICIPLE = /^[\w-]*(?:ed|en|wn|ld|lt|nt|pt|ne|id)$/i;
|
|
757
|
+
function notAbsoluteFragment(block, match, index) {
|
|
758
|
+
const [head, ...rest] = match.trim().split(/[ \t]+/);
|
|
759
|
+
let words = rest;
|
|
760
|
+
const lead = head.toLowerCase();
|
|
761
|
+
if (lead === "no" && words[0]?.toLowerCase() === "one") words = words.slice(1);
|
|
762
|
+
else if (lead === "no" && NO_IDIOM.test(words[0] ?? "")) return true;
|
|
763
|
+
if (words.some((w) => FINITE.test(w) || /['’](?:s|re|ve|ll|d)$/i.test(w))) return true;
|
|
764
|
+
if (lead === "never" && !PARTICIPLE.test(words[0] ?? "")) return true;
|
|
765
|
+
if (lead !== "no" && lead !== "never" && /^(?!else$|less$|across$)[a-z]+s$/i.test(words[0] ?? "")) return true;
|
|
766
|
+
const sentence = lineAround(block.text, index);
|
|
767
|
+
const line = sentence.trim().replace(/^[-*]\s+/, "");
|
|
768
|
+
if (NO_X_NO_Y.test(sentence) || NO_X_BADGE.test(line)) return true;
|
|
769
|
+
if (NEGATIVE_REASSURANCE.some((p) => wordList([p]).test(match))) return true;
|
|
770
|
+
return emptyState(block, match, index);
|
|
771
|
+
}
|
|
749
772
|
var alreadyBlocked = (block, match, index) => {
|
|
750
|
-
const
|
|
773
|
+
const before = block.text.slice(0, index);
|
|
774
|
+
const start = Math.max(before.search(/[^.!?\n]*$/), 0);
|
|
775
|
+
const sentence = block.text.slice(start, index + match.length);
|
|
751
776
|
return NEGATION.some((pattern) => pattern.test(sentence));
|
|
752
777
|
};
|
|
753
778
|
var DATA_ABSENCE = /\b(?:yet|found|available|configured|selected|assigned|scheduled|recorded|on file|to show|to display|results|match|matches|matching|data|in this period)\b/i;
|
|
@@ -1176,7 +1201,9 @@ var COPY_TELLS = [
|
|
|
1176
1201
|
f("content/report.md", "Edge matters for game servers, not registration."),
|
|
1177
1202
|
f("content/report.md", "Age 18 is the league's own threshold, not a legal one."),
|
|
1178
1203
|
f("content/report.md", "Cloud computing means renting computing over the internet instead of owning it."),
|
|
1179
|
-
f("content/home.md", "Real Square data, not a vanity metric.")
|
|
1204
|
+
f("content/home.md", "Real Square data, not a vanity metric."),
|
|
1205
|
+
// Reported even straight after a sentence the blocking rule reports.
|
|
1206
|
+
f("content/report.md", "That means renting services, not running servers.\n\nEdge matters for game servers, not registration.")
|
|
1180
1207
|
],
|
|
1181
1208
|
pass: [
|
|
1182
1209
|
f("content/home.md", "Rewire, retile and replumb a bathroom in five days."),
|
|
@@ -1280,6 +1307,46 @@ var COPY_TELLS = [
|
|
|
1280
1307
|
]
|
|
1281
1308
|
}
|
|
1282
1309
|
},
|
|
1310
|
+
{
|
|
1311
|
+
id: "absolute-negation",
|
|
1312
|
+
name: "Absolute negation fragment",
|
|
1313
|
+
generation: 2,
|
|
1314
|
+
severity: "warn",
|
|
1315
|
+
surface: "copy",
|
|
1316
|
+
why: "'never pre-ticked', 'no servers', 'nothing to buy or run': a verbless fragment that opens on never, nothing, nobody or 'no' + noun. Read aloud it sounds rigid, it promises an absolute the copy rarely means, and it defines the thing by what it is not.",
|
|
1317
|
+
fix: "Say what happens, as a sentence: 'A guardian ticks the consent box in the email.', 'Microsoft runs the servers.' Keep the absolute only where it is literally true and the reader needs it, and give it a subject and a verb.",
|
|
1318
|
+
detect: phrases([ABSOLUTE_NEGATION], quoted, allText, notAbsoluteFragment),
|
|
1319
|
+
fixtures: {
|
|
1320
|
+
flag: [
|
|
1321
|
+
// From a 92EL deck that passed the gate, October 2026.
|
|
1322
|
+
f("content/deck.md", "A guardian consents by email; never pre-ticked."),
|
|
1323
|
+
f("content/deck.md", "Pay per user per month; no servers."),
|
|
1324
|
+
f("content/deck.md", "Microsoft runs it, so there is less to manage: nothing to buy or run."),
|
|
1325
|
+
f("content/deck.md", "Players register on their phone. Nothing on-site to join."),
|
|
1326
|
+
f("content/deck.md", "Each player is checked against the list once, no duplicates."),
|
|
1327
|
+
f("app/slide.tsx", `export const note = "Nobody to chase for a form.";`)
|
|
1328
|
+
],
|
|
1329
|
+
pass: [
|
|
1330
|
+
f("content/report.md", "Players never open the lists."),
|
|
1331
|
+
f("content/report.md", "Nothing is copied by hand."),
|
|
1332
|
+
f("content/report.md", "It is not tested at national scale."),
|
|
1333
|
+
f("content/report.md", "Never share your password with anyone."),
|
|
1334
|
+
f("content/report.md", "No one knows the password but the club secretary."),
|
|
1335
|
+
// A Harvard citation with no date.
|
|
1336
|
+
f("content/report.md", "Children's data needs a lawful basis (ICO, no date)."),
|
|
1337
|
+
f("content/report.md", "- Information Commissioner's Office (ICO), no date. Children and the UK GDPR."),
|
|
1338
|
+
// A form answer option.
|
|
1339
|
+
f("content/form.md", "Is the player under 18?\n\n- Yes\n- No"),
|
|
1340
|
+
f("app/form.tsx", `export const options = ["Yes", "No"];`),
|
|
1341
|
+
f("content/form.md", "No, the player is 18 or over."),
|
|
1342
|
+
f("content/home.md", "We no longer post paper forms."),
|
|
1343
|
+
f("content/report.md", "If the check fails, nothing new appears in the list."),
|
|
1344
|
+
// The blocking tells report these.
|
|
1345
|
+
f("content/home.md", "We reply within a day. No obligation, no spam."),
|
|
1346
|
+
f("app/list.tsx", `export const empty = "No results found.";`)
|
|
1347
|
+
]
|
|
1348
|
+
}
|
|
1349
|
+
},
|
|
1283
1350
|
{
|
|
1284
1351
|
id: "where-x-meets-y",
|
|
1285
1352
|
name: "'Where X meets Y'",
|
|
@@ -3,21 +3,21 @@ import {
|
|
|
3
3
|
} from "./chunk-OKVHWXYV.js";
|
|
4
4
|
import {
|
|
5
5
|
merge
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-FBGXUVZU.js";
|
|
7
|
+
import "./chunk-NK6AAY7O.js";
|
|
8
8
|
import "./chunk-JYNXGCYZ.js";
|
|
9
9
|
import {
|
|
10
10
|
SIBLING_AT,
|
|
11
11
|
compareToEstate,
|
|
12
12
|
loadEstate
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-A5OYA2BG.js";
|
|
14
14
|
import {
|
|
15
15
|
PAGE_TYPES,
|
|
16
16
|
validateDirection
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-QNX7CROK.js";
|
|
18
18
|
import {
|
|
19
19
|
loadNull
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-E2ZYFBQX.js";
|
|
21
21
|
import "./chunk-OQXTQQAU.js";
|
|
22
22
|
import {
|
|
23
23
|
COPY_FILE,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
loadExceptions,
|
|
29
29
|
parseFlags,
|
|
30
30
|
readPaths
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-CB4QDMH2.js";
|
|
32
32
|
import {
|
|
33
33
|
toJson
|
|
34
34
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -42,8 +42,8 @@ import {
|
|
|
42
42
|
fingerprint,
|
|
43
43
|
scanSource,
|
|
44
44
|
typicality
|
|
45
|
-
} from "./chunk-
|
|
46
|
-
import "./chunk-
|
|
45
|
+
} from "./chunk-A5M4AOMY.js";
|
|
46
|
+
import "./chunk-ZTHVOXZF.js";
|
|
47
47
|
import "./chunk-QYBAVU3P.js";
|
|
48
48
|
|
|
49
49
|
// src/report/cli.ts
|
|
@@ -126,7 +126,7 @@ async function runReport(command, args, io) {
|
|
|
126
126
|
if (/\.json$/i.test(target2) && existsSync(local)) {
|
|
127
127
|
snap = readSnapshot(JSON.parse(readFileSync(local, "utf8")), target2);
|
|
128
128
|
} else {
|
|
129
|
-
const { snapshotUrl } = await import("./audit-
|
|
129
|
+
const { snapshotUrl } = await import("./audit-SWLTAEDO.js");
|
|
130
130
|
snap = await snapshotUrl(target2);
|
|
131
131
|
}
|
|
132
132
|
const fp = fingerprint(snap);
|
|
@@ -4,15 +4,15 @@ import {
|
|
|
4
4
|
formatPairs,
|
|
5
5
|
loadEstate,
|
|
6
6
|
runEstate
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-A5OYA2BG.js";
|
|
8
|
+
import "./chunk-QNX7CROK.js";
|
|
9
|
+
import "./chunk-E2ZYFBQX.js";
|
|
10
10
|
import "./chunk-OQXTQQAU.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-CB4QDMH2.js";
|
|
12
12
|
import "./chunk-QIEDBL4J.js";
|
|
13
13
|
import "./chunk-WUACK2HX.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-A5M4AOMY.js";
|
|
15
|
+
import "./chunk-ZTHVOXZF.js";
|
|
16
16
|
import "./chunk-QYBAVU3P.js";
|
|
17
17
|
export {
|
|
18
18
|
formatComponentPairs,
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
expandNullTargets,
|
|
6
6
|
loadNull
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-E2ZYFBQX.js";
|
|
8
8
|
import "./chunk-OQXTQQAU.js";
|
|
9
9
|
import {
|
|
10
10
|
parseFlags
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-CB4QDMH2.js";
|
|
12
12
|
import {
|
|
13
13
|
toJson
|
|
14
14
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
fingerprint,
|
|
22
22
|
scanSource,
|
|
23
23
|
typicality
|
|
24
|
-
} from "./chunk-
|
|
25
|
-
import "./chunk-
|
|
24
|
+
} from "./chunk-A5M4AOMY.js";
|
|
25
|
+
import "./chunk-ZTHVOXZF.js";
|
|
26
26
|
import {
|
|
27
27
|
SNAPSHOT_VERSION
|
|
28
28
|
} from "./chunk-QYBAVU3P.js";
|
|
@@ -188,7 +188,7 @@ async function runCalibrate(args, io) {
|
|
|
188
188
|
if (toTake.length) {
|
|
189
189
|
io.err(`craft calibrate: taking ${toTake.length} snapshot${toTake.length === 1 ? "" : "s"}`);
|
|
190
190
|
try {
|
|
191
|
-
const { snapshotUrls } = await import("./audit-
|
|
191
|
+
const { snapshotUrls } = await import("./audit-SWLTAEDO.js");
|
|
192
192
|
const results = await snapshotUrls(toTake.map((t) => t.url));
|
|
193
193
|
results.forEach((r, i) => {
|
|
194
194
|
const { entry, keep } = toTake[i];
|
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
runHarvest,
|
|
10
10
|
runNull,
|
|
11
11
|
serveDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-E2ZYFBQX.js";
|
|
13
13
|
import "./chunk-OQXTQQAU.js";
|
|
14
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-CB4QDMH2.js";
|
|
15
15
|
import "./chunk-QIEDBL4J.js";
|
|
16
16
|
import "./chunk-WUACK2HX.js";
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-A5M4AOMY.js";
|
|
18
|
+
import "./chunk-ZTHVOXZF.js";
|
|
19
19
|
import "./chunk-QYBAVU3P.js";
|
|
20
20
|
export {
|
|
21
21
|
describeTypicality,
|
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
merge,
|
|
5
5
|
resolvePages,
|
|
6
6
|
runAudit
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-FBGXUVZU.js";
|
|
8
|
+
import "./chunk-NK6AAY7O.js";
|
|
9
9
|
import "./chunk-JYNXGCYZ.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-E2ZYFBQX.js";
|
|
11
11
|
import "./chunk-OQXTQQAU.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-CB4QDMH2.js";
|
|
13
13
|
import "./chunk-QIEDBL4J.js";
|
|
14
14
|
import "./chunk-WUACK2HX.js";
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-A5M4AOMY.js";
|
|
16
|
+
import "./chunk-ZTHVOXZF.js";
|
|
17
17
|
import "./chunk-QYBAVU3P.js";
|
|
18
18
|
export {
|
|
19
19
|
describeComponents,
|
|
@@ -16,12 +16,12 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
SOURCE_FILE,
|
|
18
18
|
readPaths
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-CB4QDMH2.js";
|
|
20
20
|
import {
|
|
21
21
|
toJson
|
|
22
22
|
} from "./chunk-QIEDBL4J.js";
|
|
23
|
-
import "./chunk-
|
|
24
|
-
import "./chunk-
|
|
23
|
+
import "./chunk-A5M4AOMY.js";
|
|
24
|
+
import "./chunk-ZTHVOXZF.js";
|
|
25
25
|
import "./chunk-QYBAVU3P.js";
|
|
26
26
|
|
|
27
27
|
// src/phone/cli.ts
|
|
@@ -19,10 +19,10 @@ import {
|
|
|
19
19
|
checkReason,
|
|
20
20
|
researchPrompt,
|
|
21
21
|
validateDirection
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-QNX7CROK.js";
|
|
23
23
|
import {
|
|
24
24
|
parseFlags
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-CB4QDMH2.js";
|
|
26
26
|
import {
|
|
27
27
|
toJson
|
|
28
28
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -38,8 +38,8 @@ import {
|
|
|
38
38
|
isCream,
|
|
39
39
|
oklchToRgb,
|
|
40
40
|
parseColour
|
|
41
|
-
} from "./chunk-
|
|
42
|
-
import "./chunk-
|
|
41
|
+
} from "./chunk-A5M4AOMY.js";
|
|
42
|
+
import "./chunk-ZTHVOXZF.js";
|
|
43
43
|
import "./chunk-QYBAVU3P.js";
|
|
44
44
|
|
|
45
45
|
// src/direction/cli.ts
|
|
@@ -484,7 +484,7 @@ var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
|
484
484
|
async function snapshotFrom(target, cwd) {
|
|
485
485
|
if (!target) return void 0;
|
|
486
486
|
if (/^https?:\/\//.test(target)) {
|
|
487
|
-
const { snapshotUrl } = await import("./audit-
|
|
487
|
+
const { snapshotUrl } = await import("./audit-SWLTAEDO.js");
|
|
488
488
|
return snapshotUrl(target);
|
|
489
489
|
}
|
|
490
490
|
return readSnapshot(readJson(resolve(cwd, target)), target);
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
run
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-CB4QDMH2.js";
|
|
5
5
|
import "./chunk-QIEDBL4J.js";
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-A5M4AOMY.js";
|
|
7
|
+
import "./chunk-ZTHVOXZF.js";
|
|
8
8
|
import "./chunk-QYBAVU3P.js";
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
package/dist/index.cjs
CHANGED
|
@@ -2160,8 +2160,33 @@ var PARALLEL_PAIR = [
|
|
|
2160
2160
|
// a verbless list as a tagline: "Service account, match-day reminders, analytics."
|
|
2161
2161
|
/^[ \t]*(?:#{1,6}[ \t]+)?[A-Z][\w'’-]*(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}(?:,[ \t]+(?!(?:and|or)\b)[\w'’-]+(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}){2,3}\.[ \t]*$/m
|
|
2162
2162
|
];
|
|
2163
|
+
var CLAUSE_START = String.raw`(?<=^[ \t]*(?:[-*][ \t]+|#{1,6}[ \t]+)?|[;:,(—–][ \t]*|[.!?][ \t]+)`;
|
|
2164
|
+
var ABSOLUTE_NEGATION = new RegExp(
|
|
2165
|
+
`${CLAUSE_START}(?:never|nothing|nobody|no[ \\t]+one|no)(?:[ \\t]+[\\w'\u2019\xA3$\u20AC-]+){1,6}(?=[ \\t]*(?:[.,;:!?)"\u201D\u2014\u2013]|$))`,
|
|
2166
|
+
"im"
|
|
2167
|
+
);
|
|
2168
|
+
var FINITE = /^(?:is|are|was|were|am|be|been|being|has|have|had|do|does|did|can|could|will|would|shall|should|may|might|must|need|needs|gets|got|goes|went|comes|came|makes|means|matters|happens|stays|changes|works|takes|says|knows|seems|feels|looks|leaves|left|appears|uses|runs|shows|sends|holds|reads|writes|opens|stores|reaches|applies|isn['’]t|aren['’]t|wasn['’]t)$/i;
|
|
2169
|
+
var NO_IDIOM = /^(?:date|longer|later|sooner|matter|more|less|fewer|other|one|doubt|wonder|thanks|problem)$/i;
|
|
2170
|
+
var PARTICIPLE = /^[\w-]*(?:ed|en|wn|ld|lt|nt|pt|ne|id)$/i;
|
|
2171
|
+
function notAbsoluteFragment(block, match, index) {
|
|
2172
|
+
const [head, ...rest] = match.trim().split(/[ \t]+/);
|
|
2173
|
+
let words3 = rest;
|
|
2174
|
+
const lead = head.toLowerCase();
|
|
2175
|
+
if (lead === "no" && words3[0]?.toLowerCase() === "one") words3 = words3.slice(1);
|
|
2176
|
+
else if (lead === "no" && NO_IDIOM.test(words3[0] ?? "")) return true;
|
|
2177
|
+
if (words3.some((w) => FINITE.test(w) || /['’](?:s|re|ve|ll|d)$/i.test(w))) return true;
|
|
2178
|
+
if (lead === "never" && !PARTICIPLE.test(words3[0] ?? "")) return true;
|
|
2179
|
+
if (lead !== "no" && lead !== "never" && /^(?!else$|less$|across$)[a-z]+s$/i.test(words3[0] ?? "")) return true;
|
|
2180
|
+
const sentence = lineAround(block.text, index);
|
|
2181
|
+
const line = sentence.trim().replace(/^[-*]\s+/, "");
|
|
2182
|
+
if (NO_X_NO_Y.test(sentence) || NO_X_BADGE.test(line)) return true;
|
|
2183
|
+
if (NEGATIVE_REASSURANCE.some((p) => wordList([p]).test(match))) return true;
|
|
2184
|
+
return emptyState(block, match, index);
|
|
2185
|
+
}
|
|
2163
2186
|
var alreadyBlocked = (block, match, index) => {
|
|
2164
|
-
const
|
|
2187
|
+
const before = block.text.slice(0, index);
|
|
2188
|
+
const start = Math.max(before.search(/[^.!?\n]*$/), 0);
|
|
2189
|
+
const sentence = block.text.slice(start, index + match.length);
|
|
2165
2190
|
return NEGATION.some((pattern2) => pattern2.test(sentence));
|
|
2166
2191
|
};
|
|
2167
2192
|
var DATA_ABSENCE = /\b(?:yet|found|available|configured|selected|assigned|scheduled|recorded|on file|to show|to display|results|match|matches|matching|data|in this period)\b/i;
|
|
@@ -2590,7 +2615,9 @@ var COPY_TELLS = [
|
|
|
2590
2615
|
f("content/report.md", "Edge matters for game servers, not registration."),
|
|
2591
2616
|
f("content/report.md", "Age 18 is the league's own threshold, not a legal one."),
|
|
2592
2617
|
f("content/report.md", "Cloud computing means renting computing over the internet instead of owning it."),
|
|
2593
|
-
f("content/home.md", "Real Square data, not a vanity metric.")
|
|
2618
|
+
f("content/home.md", "Real Square data, not a vanity metric."),
|
|
2619
|
+
// Reported even straight after a sentence the blocking rule reports.
|
|
2620
|
+
f("content/report.md", "That means renting services, not running servers.\n\nEdge matters for game servers, not registration.")
|
|
2594
2621
|
],
|
|
2595
2622
|
pass: [
|
|
2596
2623
|
f("content/home.md", "Rewire, retile and replumb a bathroom in five days."),
|
|
@@ -2694,6 +2721,46 @@ var COPY_TELLS = [
|
|
|
2694
2721
|
]
|
|
2695
2722
|
}
|
|
2696
2723
|
},
|
|
2724
|
+
{
|
|
2725
|
+
id: "absolute-negation",
|
|
2726
|
+
name: "Absolute negation fragment",
|
|
2727
|
+
generation: 2,
|
|
2728
|
+
severity: "warn",
|
|
2729
|
+
surface: "copy",
|
|
2730
|
+
why: "'never pre-ticked', 'no servers', 'nothing to buy or run': a verbless fragment that opens on never, nothing, nobody or 'no' + noun. Read aloud it sounds rigid, it promises an absolute the copy rarely means, and it defines the thing by what it is not.",
|
|
2731
|
+
fix: "Say what happens, as a sentence: 'A guardian ticks the consent box in the email.', 'Microsoft runs the servers.' Keep the absolute only where it is literally true and the reader needs it, and give it a subject and a verb.",
|
|
2732
|
+
detect: phrases([ABSOLUTE_NEGATION], quoted, allText, notAbsoluteFragment),
|
|
2733
|
+
fixtures: {
|
|
2734
|
+
flag: [
|
|
2735
|
+
// From a 92EL deck that passed the gate, October 2026.
|
|
2736
|
+
f("content/deck.md", "A guardian consents by email; never pre-ticked."),
|
|
2737
|
+
f("content/deck.md", "Pay per user per month; no servers."),
|
|
2738
|
+
f("content/deck.md", "Microsoft runs it, so there is less to manage: nothing to buy or run."),
|
|
2739
|
+
f("content/deck.md", "Players register on their phone. Nothing on-site to join."),
|
|
2740
|
+
f("content/deck.md", "Each player is checked against the list once, no duplicates."),
|
|
2741
|
+
f("app/slide.tsx", `export const note = "Nobody to chase for a form.";`)
|
|
2742
|
+
],
|
|
2743
|
+
pass: [
|
|
2744
|
+
f("content/report.md", "Players never open the lists."),
|
|
2745
|
+
f("content/report.md", "Nothing is copied by hand."),
|
|
2746
|
+
f("content/report.md", "It is not tested at national scale."),
|
|
2747
|
+
f("content/report.md", "Never share your password with anyone."),
|
|
2748
|
+
f("content/report.md", "No one knows the password but the club secretary."),
|
|
2749
|
+
// A Harvard citation with no date.
|
|
2750
|
+
f("content/report.md", "Children's data needs a lawful basis (ICO, no date)."),
|
|
2751
|
+
f("content/report.md", "- Information Commissioner's Office (ICO), no date. Children and the UK GDPR."),
|
|
2752
|
+
// A form answer option.
|
|
2753
|
+
f("content/form.md", "Is the player under 18?\n\n- Yes\n- No"),
|
|
2754
|
+
f("app/form.tsx", `export const options = ["Yes", "No"];`),
|
|
2755
|
+
f("content/form.md", "No, the player is 18 or over."),
|
|
2756
|
+
f("content/home.md", "We no longer post paper forms."),
|
|
2757
|
+
f("content/report.md", "If the check fails, nothing new appears in the list."),
|
|
2758
|
+
// The blocking tells report these.
|
|
2759
|
+
f("content/home.md", "We reply within a day. No obligation, no spam."),
|
|
2760
|
+
f("app/list.tsx", `export const empty = "No results found.";`)
|
|
2761
|
+
]
|
|
2762
|
+
}
|
|
2763
|
+
},
|
|
2697
2764
|
{
|
|
2698
2765
|
id: "where-x-meets-y",
|
|
2699
2766
|
name: "'Where X meets Y'",
|
|
@@ -5634,6 +5701,8 @@ var HOUSE = {
|
|
|
5634
5701
|
// New 2026-10-23 (founder, from 92EL deliverables): review, since a short slide label is often fine.
|
|
5635
5702
|
"parallel-pair": { tier: "review", label: "verbless parallel pair, write the sentence it stands for" },
|
|
5636
5703
|
"trailing-contrast": { tier: "review", label: "trailing contrast. Say the positive claim, unless it is a disclaimer the reader needs" },
|
|
5704
|
+
// New 2026-10-26 (founder, from a 92EL deck): review first, like every new tell, until estate hits are read.
|
|
5705
|
+
"absolute-negation": { tier: "review", label: "absolute negation fragment. Say what happens, as a sentence with a subject and a verb" },
|
|
5637
5706
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
5638
5707
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
5639
5708
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -6866,7 +6935,7 @@ var IMAGERY_TELLS = [
|
|
|
6866
6935
|
];
|
|
6867
6936
|
|
|
6868
6937
|
// src/character/check.ts
|
|
6869
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
6938
|
+
var CATALOGUE_VERSION = "2026.10.9";
|
|
6870
6939
|
var CATALOGUE = [
|
|
6871
6940
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
6872
6941
|
...SPECIFICITY_COPY_TELLS,
|
package/dist/index.d.cts
CHANGED
|
@@ -1252,7 +1252,7 @@ interface CheckReport {
|
|
|
1252
1252
|
* Bumped whenever an entry is added, removed or its detection changes, so a
|
|
1253
1253
|
* report can say which list it was judged against.
|
|
1254
1254
|
*/
|
|
1255
|
-
declare const CATALOGUE_VERSION = "2026.10.
|
|
1255
|
+
declare const CATALOGUE_VERSION = "2026.10.9";
|
|
1256
1256
|
declare const CATALOGUE: readonly Tell[];
|
|
1257
1257
|
declare function tellById(id: string): Tell | undefined;
|
|
1258
1258
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1252,7 +1252,7 @@ interface CheckReport {
|
|
|
1252
1252
|
* Bumped whenever an entry is added, removed or its detection changes, so a
|
|
1253
1253
|
* report can say which list it was judged against.
|
|
1254
1254
|
*/
|
|
1255
|
-
declare const CATALOGUE_VERSION = "2026.10.
|
|
1255
|
+
declare const CATALOGUE_VERSION = "2026.10.9";
|
|
1256
1256
|
declare const CATALOGUE: readonly Tell[];
|
|
1257
1257
|
declare function tellById(id: string): Tell | undefined;
|
|
1258
1258
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1841,8 +1841,33 @@ var PARALLEL_PAIR = [
|
|
|
1841
1841
|
// a verbless list as a tagline: "Service account, match-day reminders, analytics."
|
|
1842
1842
|
/^[ \t]*(?:#{1,6}[ \t]+)?[A-Z][\w'’-]*(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}(?:,[ \t]+(?!(?:and|or)\b)[\w'’-]+(?:[ \t]+(?!(?:and|or)\b)[\w'’-]+){0,2}){2,3}\.[ \t]*$/m
|
|
1843
1843
|
];
|
|
1844
|
+
var CLAUSE_START = String.raw`(?<=^[ \t]*(?:[-*][ \t]+|#{1,6}[ \t]+)?|[;:,(—–][ \t]*|[.!?][ \t]+)`;
|
|
1845
|
+
var ABSOLUTE_NEGATION = new RegExp(
|
|
1846
|
+
`${CLAUSE_START}(?:never|nothing|nobody|no[ \\t]+one|no)(?:[ \\t]+[\\w'\u2019\xA3$\u20AC-]+){1,6}(?=[ \\t]*(?:[.,;:!?)"\u201D\u2014\u2013]|$))`,
|
|
1847
|
+
"im"
|
|
1848
|
+
);
|
|
1849
|
+
var FINITE = /^(?:is|are|was|were|am|be|been|being|has|have|had|do|does|did|can|could|will|would|shall|should|may|might|must|need|needs|gets|got|goes|went|comes|came|makes|means|matters|happens|stays|changes|works|takes|says|knows|seems|feels|looks|leaves|left|appears|uses|runs|shows|sends|holds|reads|writes|opens|stores|reaches|applies|isn['’]t|aren['’]t|wasn['’]t)$/i;
|
|
1850
|
+
var NO_IDIOM = /^(?:date|longer|later|sooner|matter|more|less|fewer|other|one|doubt|wonder|thanks|problem)$/i;
|
|
1851
|
+
var PARTICIPLE = /^[\w-]*(?:ed|en|wn|ld|lt|nt|pt|ne|id)$/i;
|
|
1852
|
+
function notAbsoluteFragment(block, match, index) {
|
|
1853
|
+
const [head, ...rest] = match.trim().split(/[ \t]+/);
|
|
1854
|
+
let words3 = rest;
|
|
1855
|
+
const lead = head.toLowerCase();
|
|
1856
|
+
if (lead === "no" && words3[0]?.toLowerCase() === "one") words3 = words3.slice(1);
|
|
1857
|
+
else if (lead === "no" && NO_IDIOM.test(words3[0] ?? "")) return true;
|
|
1858
|
+
if (words3.some((w) => FINITE.test(w) || /['’](?:s|re|ve|ll|d)$/i.test(w))) return true;
|
|
1859
|
+
if (lead === "never" && !PARTICIPLE.test(words3[0] ?? "")) return true;
|
|
1860
|
+
if (lead !== "no" && lead !== "never" && /^(?!else$|less$|across$)[a-z]+s$/i.test(words3[0] ?? "")) return true;
|
|
1861
|
+
const sentence = lineAround(block.text, index);
|
|
1862
|
+
const line = sentence.trim().replace(/^[-*]\s+/, "");
|
|
1863
|
+
if (NO_X_NO_Y.test(sentence) || NO_X_BADGE.test(line)) return true;
|
|
1864
|
+
if (NEGATIVE_REASSURANCE.some((p) => wordList([p]).test(match))) return true;
|
|
1865
|
+
return emptyState(block, match, index);
|
|
1866
|
+
}
|
|
1844
1867
|
var alreadyBlocked = (block, match, index) => {
|
|
1845
|
-
const
|
|
1868
|
+
const before = block.text.slice(0, index);
|
|
1869
|
+
const start = Math.max(before.search(/[^.!?\n]*$/), 0);
|
|
1870
|
+
const sentence = block.text.slice(start, index + match.length);
|
|
1846
1871
|
return NEGATION.some((pattern2) => pattern2.test(sentence));
|
|
1847
1872
|
};
|
|
1848
1873
|
var DATA_ABSENCE = /\b(?:yet|found|available|configured|selected|assigned|scheduled|recorded|on file|to show|to display|results|match|matches|matching|data|in this period)\b/i;
|
|
@@ -2271,7 +2296,9 @@ var COPY_TELLS = [
|
|
|
2271
2296
|
f("content/report.md", "Edge matters for game servers, not registration."),
|
|
2272
2297
|
f("content/report.md", "Age 18 is the league's own threshold, not a legal one."),
|
|
2273
2298
|
f("content/report.md", "Cloud computing means renting computing over the internet instead of owning it."),
|
|
2274
|
-
f("content/home.md", "Real Square data, not a vanity metric.")
|
|
2299
|
+
f("content/home.md", "Real Square data, not a vanity metric."),
|
|
2300
|
+
// Reported even straight after a sentence the blocking rule reports.
|
|
2301
|
+
f("content/report.md", "That means renting services, not running servers.\n\nEdge matters for game servers, not registration.")
|
|
2275
2302
|
],
|
|
2276
2303
|
pass: [
|
|
2277
2304
|
f("content/home.md", "Rewire, retile and replumb a bathroom in five days."),
|
|
@@ -2375,6 +2402,46 @@ var COPY_TELLS = [
|
|
|
2375
2402
|
]
|
|
2376
2403
|
}
|
|
2377
2404
|
},
|
|
2405
|
+
{
|
|
2406
|
+
id: "absolute-negation",
|
|
2407
|
+
name: "Absolute negation fragment",
|
|
2408
|
+
generation: 2,
|
|
2409
|
+
severity: "warn",
|
|
2410
|
+
surface: "copy",
|
|
2411
|
+
why: "'never pre-ticked', 'no servers', 'nothing to buy or run': a verbless fragment that opens on never, nothing, nobody or 'no' + noun. Read aloud it sounds rigid, it promises an absolute the copy rarely means, and it defines the thing by what it is not.",
|
|
2412
|
+
fix: "Say what happens, as a sentence: 'A guardian ticks the consent box in the email.', 'Microsoft runs the servers.' Keep the absolute only where it is literally true and the reader needs it, and give it a subject and a verb.",
|
|
2413
|
+
detect: phrases([ABSOLUTE_NEGATION], quoted, allText, notAbsoluteFragment),
|
|
2414
|
+
fixtures: {
|
|
2415
|
+
flag: [
|
|
2416
|
+
// From a 92EL deck that passed the gate, October 2026.
|
|
2417
|
+
f("content/deck.md", "A guardian consents by email; never pre-ticked."),
|
|
2418
|
+
f("content/deck.md", "Pay per user per month; no servers."),
|
|
2419
|
+
f("content/deck.md", "Microsoft runs it, so there is less to manage: nothing to buy or run."),
|
|
2420
|
+
f("content/deck.md", "Players register on their phone. Nothing on-site to join."),
|
|
2421
|
+
f("content/deck.md", "Each player is checked against the list once, no duplicates."),
|
|
2422
|
+
f("app/slide.tsx", `export const note = "Nobody to chase for a form.";`)
|
|
2423
|
+
],
|
|
2424
|
+
pass: [
|
|
2425
|
+
f("content/report.md", "Players never open the lists."),
|
|
2426
|
+
f("content/report.md", "Nothing is copied by hand."),
|
|
2427
|
+
f("content/report.md", "It is not tested at national scale."),
|
|
2428
|
+
f("content/report.md", "Never share your password with anyone."),
|
|
2429
|
+
f("content/report.md", "No one knows the password but the club secretary."),
|
|
2430
|
+
// A Harvard citation with no date.
|
|
2431
|
+
f("content/report.md", "Children's data needs a lawful basis (ICO, no date)."),
|
|
2432
|
+
f("content/report.md", "- Information Commissioner's Office (ICO), no date. Children and the UK GDPR."),
|
|
2433
|
+
// A form answer option.
|
|
2434
|
+
f("content/form.md", "Is the player under 18?\n\n- Yes\n- No"),
|
|
2435
|
+
f("app/form.tsx", `export const options = ["Yes", "No"];`),
|
|
2436
|
+
f("content/form.md", "No, the player is 18 or over."),
|
|
2437
|
+
f("content/home.md", "We no longer post paper forms."),
|
|
2438
|
+
f("content/report.md", "If the check fails, nothing new appears in the list."),
|
|
2439
|
+
// The blocking tells report these.
|
|
2440
|
+
f("content/home.md", "We reply within a day. No obligation, no spam."),
|
|
2441
|
+
f("app/list.tsx", `export const empty = "No results found.";`)
|
|
2442
|
+
]
|
|
2443
|
+
}
|
|
2444
|
+
},
|
|
2378
2445
|
{
|
|
2379
2446
|
id: "where-x-meets-y",
|
|
2380
2447
|
name: "'Where X meets Y'",
|
|
@@ -5315,6 +5382,8 @@ var HOUSE = {
|
|
|
5315
5382
|
// New 2026-10-23 (founder, from 92EL deliverables): review, since a short slide label is often fine.
|
|
5316
5383
|
"parallel-pair": { tier: "review", label: "verbless parallel pair, write the sentence it stands for" },
|
|
5317
5384
|
"trailing-contrast": { tier: "review", label: "trailing contrast. Say the positive claim, unless it is a disclaimer the reader needs" },
|
|
5385
|
+
// New 2026-10-26 (founder, from a 92EL deck): review first, like every new tell, until estate hits are read.
|
|
5386
|
+
"absolute-negation": { tier: "review", label: "absolute negation fragment. Say what happens, as a sentence with a subject and a verb" },
|
|
5318
5387
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
5319
5388
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
5320
5389
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -6547,7 +6616,7 @@ var IMAGERY_TELLS = [
|
|
|
6547
6616
|
];
|
|
6548
6617
|
|
|
6549
6618
|
// src/character/check.ts
|
|
6550
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
6619
|
+
var CATALOGUE_VERSION = "2026.10.9";
|
|
6551
6620
|
var CATALOGUE = [
|
|
6552
6621
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
6553
6622
|
...SPECIFICITY_COPY_TELLS,
|
|
@@ -16,13 +16,13 @@ import {
|
|
|
16
16
|
} from "./chunk-XSKLVEI6.js";
|
|
17
17
|
import {
|
|
18
18
|
loadPlaywright
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-NK6AAY7O.js";
|
|
20
20
|
import "./chunk-JYNXGCYZ.js";
|
|
21
21
|
import {
|
|
22
22
|
toJson
|
|
23
23
|
} from "./chunk-QIEDBL4J.js";
|
|
24
|
-
import "./chunk-
|
|
25
|
-
import "./chunk-
|
|
24
|
+
import "./chunk-A5M4AOMY.js";
|
|
25
|
+
import "./chunk-ZTHVOXZF.js";
|
|
26
26
|
import "./chunk-QYBAVU3P.js";
|
|
27
27
|
|
|
28
28
|
// src/phone/viewports-cli.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@domandigital/craft",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The Doman Digital house craft standard as numbers: OKLCh colour ramps that snap to existing anchors, derived semantic tokens with measured WCAG and APCA contrast, one shared motion vocabulary for CSS and JS, and a base stylesheet. Zero dependencies.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/word-lists.json
CHANGED