@domandigital/craft 0.23.1 → 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 +6 -0
- package/CHARACTER.md +2 -1
- package/COPY.md +1 -0
- package/dist/{audit-DESBVHQJ.js → audit-SWLTAEDO.js} +3 -3
- package/dist/audit.cjs +66 -1
- package/dist/audit.js +66 -1
- package/dist/{chunk-GA3AJN67.js → chunk-A5M4AOMY.js} +4 -2
- package/dist/{chunk-LRWKKJIW.js → chunk-A5OYA2BG.js} +5 -5
- package/dist/{chunk-V3GJSISR.js → chunk-CB4QDMH2.js} +14 -14
- package/dist/{chunk-GDDBCHQR.js → chunk-E2ZYFBQX.js} +5 -5
- package/dist/{chunk-M5QSMIT6.js → chunk-FBGXUVZU.js} +4 -4
- package/dist/{chunk-ZV3FPQPM.js → chunk-NK6AAY7O.js} +1 -1
- package/dist/{chunk-7DPB5G4G.js → chunk-QNX7CROK.js} +2 -2
- package/dist/{chunk-EHMEMSM2.js → chunk-ZTHVOXZF.js} +63 -0
- package/dist/{cli-7IBKMUMM.js → cli-7IAFZ6CV.js} +9 -9
- package/dist/{cli-RXXGSKD6.js → cli-ATLKYXPU.js} +6 -6
- package/dist/{cli-FCFRPPHZ.js → cli-BA3IVSMR.js} +5 -5
- package/dist/{cli-YOSHHLIV.js → cli-BP4JLNAU.js} +1 -1
- package/dist/{cli-36YQA6BP.js → cli-O2IEHB5Q.js} +4 -4
- package/dist/{cli-3PFEOEQI.js → cli-T5L55IWF.js} +6 -6
- package/dist/{cli-SOH4QMMW.js → cli-UPO4PR3Z.js} +3 -3
- package/dist/{cli-TP5ZMJOL.js → cli-ZMLKL2NT.js} +5 -5
- package/dist/cli.js +3 -3
- package/dist/index.cjs +66 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +66 -1
- package/dist/{viewports-cli-ET664I2Y.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,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.23.1
|
|
4
10
|
|
|
5
11
|
### Patch 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.8`, 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,6 +358,29 @@ 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
385
|
const before = block.text.slice(0, index);
|
|
363
386
|
const start = Math.max(before.search(/[^.!?\n]*$/), 0);
|
|
@@ -896,6 +919,46 @@ var COPY_TELLS = [
|
|
|
896
919
|
]
|
|
897
920
|
}
|
|
898
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
|
+
},
|
|
899
962
|
{
|
|
900
963
|
id: "where-x-meets-y",
|
|
901
964
|
name: "'Where X meets Y'",
|
|
@@ -3499,6 +3562,8 @@ var HOUSE = {
|
|
|
3499
3562
|
// New 2026-10-23 (founder, from 92EL deliverables): review, since a short slide label is often fine.
|
|
3500
3563
|
"parallel-pair": { tier: "review", label: "verbless parallel pair, write the sentence it stands for" },
|
|
3501
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" },
|
|
3502
3567
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
3503
3568
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
3504
3569
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -4267,7 +4332,7 @@ var IMAGERY_TELLS = [
|
|
|
4267
4332
|
];
|
|
4268
4333
|
|
|
4269
4334
|
// src/character/check.ts
|
|
4270
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
4335
|
+
var CATALOGUE_VERSION = "2026.10.9";
|
|
4271
4336
|
var CATALOGUE = [
|
|
4272
4337
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
4273
4338
|
...SPECIFICITY_COPY_TELLS,
|
package/dist/audit.js
CHANGED
|
@@ -323,6 +323,29 @@ 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
350
|
const before = block.text.slice(0, index);
|
|
328
351
|
const start = Math.max(before.search(/[^.!?\n]*$/), 0);
|
|
@@ -861,6 +884,46 @@ var COPY_TELLS = [
|
|
|
861
884
|
]
|
|
862
885
|
}
|
|
863
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
|
+
},
|
|
864
927
|
{
|
|
865
928
|
id: "where-x-meets-y",
|
|
866
929
|
name: "'Where X meets Y'",
|
|
@@ -3464,6 +3527,8 @@ var HOUSE = {
|
|
|
3464
3527
|
// New 2026-10-23 (founder, from 92EL deliverables): review, since a short slide label is often fine.
|
|
3465
3528
|
"parallel-pair": { tier: "review", label: "verbless parallel pair, write the sentence it stands for" },
|
|
3466
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" },
|
|
3467
3532
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
3468
3533
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
3469
3534
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -4232,7 +4297,7 @@ var IMAGERY_TELLS = [
|
|
|
4232
4297
|
];
|
|
4233
4298
|
|
|
4234
4299
|
// src/character/check.ts
|
|
4235
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
4300
|
+
var CATALOGUE_VERSION = "2026.10.9";
|
|
4236
4301
|
var CATALOGUE = [
|
|
4237
4302
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
4238
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,6 +746,29 @@ 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
773
|
const before = block.text.slice(0, index);
|
|
751
774
|
const start = Math.max(before.search(/[^.!?\n]*$/), 0);
|
|
@@ -1284,6 +1307,46 @@ var COPY_TELLS = [
|
|
|
1284
1307
|
]
|
|
1285
1308
|
}
|
|
1286
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
|
+
},
|
|
1287
1350
|
{
|
|
1288
1351
|
id: "where-x-meets-y",
|
|
1289
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,6 +2160,29 @@ 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
2187
|
const before = block.text.slice(0, index);
|
|
2165
2188
|
const start = Math.max(before.search(/[^.!?\n]*$/), 0);
|
|
@@ -2698,6 +2721,46 @@ var COPY_TELLS = [
|
|
|
2698
2721
|
]
|
|
2699
2722
|
}
|
|
2700
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
|
+
},
|
|
2701
2764
|
{
|
|
2702
2765
|
id: "where-x-meets-y",
|
|
2703
2766
|
name: "'Where X meets Y'",
|
|
@@ -5638,6 +5701,8 @@ var HOUSE = {
|
|
|
5638
5701
|
// New 2026-10-23 (founder, from 92EL deliverables): review, since a short slide label is often fine.
|
|
5639
5702
|
"parallel-pair": { tier: "review", label: "verbless parallel pair, write the sentence it stands for" },
|
|
5640
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" },
|
|
5641
5706
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
5642
5707
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
5643
5708
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -6870,7 +6935,7 @@ var IMAGERY_TELLS = [
|
|
|
6870
6935
|
];
|
|
6871
6936
|
|
|
6872
6937
|
// src/character/check.ts
|
|
6873
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
6938
|
+
var CATALOGUE_VERSION = "2026.10.9";
|
|
6874
6939
|
var CATALOGUE = [
|
|
6875
6940
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
6876
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,6 +1841,29 @@ 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
1868
|
const before = block.text.slice(0, index);
|
|
1846
1869
|
const start = Math.max(before.search(/[^.!?\n]*$/), 0);
|
|
@@ -2379,6 +2402,46 @@ var COPY_TELLS = [
|
|
|
2379
2402
|
]
|
|
2380
2403
|
}
|
|
2381
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
|
+
},
|
|
2382
2445
|
{
|
|
2383
2446
|
id: "where-x-meets-y",
|
|
2384
2447
|
name: "'Where X meets Y'",
|
|
@@ -5319,6 +5382,8 @@ var HOUSE = {
|
|
|
5319
5382
|
// New 2026-10-23 (founder, from 92EL deliverables): review, since a short slide label is often fine.
|
|
5320
5383
|
"parallel-pair": { tier: "review", label: "verbless parallel pair, write the sentence it stands for" },
|
|
5321
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" },
|
|
5322
5387
|
"ai-vocabulary": { tier: "review", label: "AI vocabulary" },
|
|
5323
5388
|
"stock-phrase": { tier: "review", label: "stock phrase" },
|
|
5324
5389
|
"hollow-imperative": { tier: "review", label: "hollow imperative" },
|
|
@@ -6551,7 +6616,7 @@ var IMAGERY_TELLS = [
|
|
|
6551
6616
|
];
|
|
6552
6617
|
|
|
6553
6618
|
// src/character/check.ts
|
|
6554
|
-
var CATALOGUE_VERSION = "2026.10.
|
|
6619
|
+
var CATALOGUE_VERSION = "2026.10.9";
|
|
6555
6620
|
var CATALOGUE = [
|
|
6556
6621
|
...[...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map((t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t),
|
|
6557
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