@domandigital/craft 0.22.0 → 0.23.0

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CHARACTER.md +138 -5
  3. package/COPY.md +35 -9
  4. package/PRINCIPLES.md +9 -0
  5. package/README.md +6 -0
  6. package/design-review.schema.json +156 -0
  7. package/dist/{audit-LQ2TDISV.js → audit-CRTC7JSF.js} +3 -3
  8. package/dist/audit.cjs +365 -15
  9. package/dist/audit.js +365 -15
  10. package/dist/{chunk-DE2ZAV4G.js → chunk-3LCBLDAO.js} +171 -3
  11. package/dist/{chunk-6PZG4MJS.js → chunk-3LHIKF5L.js} +5 -5
  12. package/dist/{chunk-G47ZCBD5.js → chunk-4NON4PVK.js} +2 -2
  13. package/dist/{chunk-6DRBFDWT.js → chunk-A7KUKNX6.js} +1 -1
  14. package/dist/{chunk-OVWVKMPS.js → chunk-KGEJ2TGT.js} +14 -14
  15. package/dist/{chunk-ONA73THJ.js → chunk-KIBDFRHT.js} +195 -13
  16. package/dist/{chunk-ZS7PQNJU.js → chunk-OTQQTXXK.js} +4 -4
  17. package/dist/{chunk-RBQCGDQQ.js → chunk-Y2LGYHJ5.js} +5 -5
  18. package/dist/{cli-R2HBESHI.js → cli-E2CFVIGZ.js} +5 -5
  19. package/dist/{cli-SZ4SZLUN.js → cli-HWGSNSDD.js} +4 -4
  20. package/dist/{cli-7DL4BKA7.js → cli-IYRJZWMN.js} +6 -6
  21. package/dist/{cli-YNIJOEJK.js → cli-RHU3MAZY.js} +3 -3
  22. package/dist/{cli-AM4FLXGM.js → cli-ROVZDHVR.js} +5 -5
  23. package/dist/{cli-T7A3BU5J.js → cli-TPV2EIIN.js} +9 -9
  24. package/dist/{cli-WZA5IXSR.js → cli-X3RZE4GM.js} +6 -6
  25. package/dist/{cli-7QL26YPR.js → cli-YRRBBU27.js} +1 -1
  26. package/dist/cli.js +3 -3
  27. package/dist/index.cjs +368 -15
  28. package/dist/index.d.cts +49 -3
  29. package/dist/index.d.ts +49 -3
  30. package/dist/index.js +367 -15
  31. package/dist/{viewports-cli-F7KJIVZL.js → viewports-cli-EAUVLHFJ.js} +3 -3
  32. package/package.json +1 -1
  33. package/word-lists.json +11 -2
@@ -616,7 +616,17 @@ var REVIEW_PHRASES = [
616
616
  "by far the most",
617
617
  "hands down the",
618
618
  "arguably the best",
619
- "bar none"
619
+ "bar none",
620
+ // "plain" as a self-description, the way "clear" is: say what the reader gets.
621
+ "plain answer",
622
+ "plain answers",
623
+ "plain recommendation",
624
+ "plain explanation",
625
+ "plain explanations",
626
+ "plain advice",
627
+ "plain guidance",
628
+ "plain-speaking",
629
+ "plain speaking"
620
630
  ];
621
631
  var SALES_TAGLINES = [
622
632
  /\bwhere it (?:really |truly )?(?:counts|matters)(?: most)?\b/i,
@@ -672,6 +682,34 @@ var CLIPPED = [
672
682
  // "Alright Carlo, built you a proper site": the "I" left out
673
683
  /(?<=^|[\n>"“]|[.!?—–-]\s*|,\s+)(?:[Bb]uilt|[Mm]ade|[Pp]ut together|[Kk]nocked up)\s+(?:you|your\s+\w+|the\s+\w+)\s+an?\b/
674
684
  ];
685
+ var PLAIN_FAMILY = [
686
+ /\b(?:plain english|explained plainly|plainly explained)\b/i,
687
+ /\bplainly\b(?!\s+marked\b)/i,
688
+ /\bin\s+plain\s+(?:terms|language|words|speak)\b/i
689
+ ];
690
+ var CONFESSION = [
691
+ // "What this is and is not:", "what we are and aren't"
692
+ /\bwhat\s+(?:this|it|we|our\s+\w+)\s+(?:is|are)\s+and\s+(?:is\s+not|isn['’]t|are\s+not|aren['’]t)\b/i,
693
+ // "What we are not", a label on its own; not "what we are not responsible for"
694
+ /(?<!\band\s)\bwhat\s+we(?:\s+are|['’]re)\s+not\b(?!\s+(?:responsible|liable|able|allowed|sure)\b)/i,
695
+ /\bwhat\s+we\s+(?:don['’]t|do\s+not)\s+do\b/i,
696
+ // "We have not yet built and run", "we haven't yet worked with"
697
+ /\bwe(?:\s+have|['’]ve)\s+not\s+yet\s+(?:built|run|done|worked|delivered|shipped|launched)\b/i,
698
+ /\bwe\s+haven['’]t\s+(?:yet\s+)?(?:built|run|done|worked\s+with|delivered)\b/i,
699
+ /\bwhat\s+we\s+have\s+and\s+(?:haven['’]t|have\s+not)\s+(?:done|built|run)\b/i,
700
+ // "Our clinic experience so far is one practice"
701
+ /\bour\s+(?:[\w-]+\s+){0,3}experience\s+so\s+far\s+(?:is|has\s+been|covers)\b/i,
702
+ /\bso\s+far\s+(?:is|has\s+been)\s+(?:one|a\s+single|just\s+one)\b/i,
703
+ /\bwhere\s+our\s+experience\s+(?:applies|ends|stops|runs\s+out)\b/i,
704
+ /\bwhat\s+we\s+(?:would|['’]d|will|still)\s+need\s+to\s+learn\b/i,
705
+ // "what we would and wouldn't take on", "what we won't take on"
706
+ /\b(?:would|will)\s+and\s+(?:wouldn['’]t|would\s+not|won['’]t|will\s+not)\s+take\s+on\b/i,
707
+ /\bwhat\s+we\s+(?:won['’]t|wouldn['’]t|would\s+not|will\s+not)\s+take\s+on\b/i,
708
+ // "Who we don't work with", "who this is not for", "we're not for everyone"
709
+ /\bwho\s+we\s+(?:don['’]t|do\s+not|won['’]t|will\s+not)\s+(?:work\s+with|serve|help|take\s+on)\b/i,
710
+ /\bwho\s+(?:this|it|we)\s+(?:is|are|['’]re)\s+not\s+for\b|\bwho\s+(?:this|it)\s+isn['’]t\s+for\b/i,
711
+ /\b(?:we|this)\s+(?:are|is|['’]re|['’]s)\s+not\s+(?:for\s+everyone|the\s+right\s+fit\s+for)\b/i
712
+ ];
675
713
  var NEGATION = [
676
714
  // "It's not just a haircut, it's an experience."
677
715
  /\b(?:it['’]?s|it is|this is|this isn['’]t|we['’]re|we are|they['’]re)\s+(?:not|more than)\s+(?:just|only|merely|simply)?\s*[^.!?\n]{1,60}?[,;.—-]\s*(?:it['’]?s|it is|this is|we['’]re|we are|they['’]re)\b/i,
@@ -685,8 +723,33 @@ var NEGATION = [
685
723
  /\b(?:is|are|was|were)(?:n’t|n't)\b[^.;]{2,60},\s*(?:it|they|that|this)(?:’s|'s|’re|'re)\b/i,
686
724
  /\bless\s+about\b[^.;]{2,50}\bmore\s+about\b/i,
687
725
  // parallel participle, no repeated subject: "dispatched from here, not shipped in from Seoul"
688
- /\b\w{3,}ed\b[^,.;:]{0,40},\s*not\s+\w{3,}ed\b/i
726
+ /\b\w{3,}ed\b[^,.;:]{0,40},\s*not\s+\w{3,}ed\b/i,
727
+ // parallel gerund: "renting Microsoft's services, not running servers". The
728
+ // -thing pronouns and the -ing prepositions are not verbs.
729
+ /\b(?!(?:some|no|any|every)thing\b)\w{3,}ing\b[^,.;:]{0,40},\s*not\s+(?!(?:some|no|any|every)thing\b|(?:during|according|including|regarding|morning|evening)\b)\w{3,}ing\b/i
730
+ ];
731
+ var SOFT_CONTRAST = [
732
+ // ", not registration." closing the sentence. Not when "not" negates what follows.
733
+ /,\s*not\s+(?!(?:yet|least|only|just|merely|simply|to|all|always|even|ever|necessarily|really|quite|exactly|enough|sure|that|because|so|much|many|once|often|every|including|counting|unlike|until|if|when|unless|though|although|or|and|but)\b)(?:(?:a|an|the|your|our|their|its|his|her|my|any)\s+)?[\w'’-]+(?:\s+[\w'’-]+){0,3}(?=\s*(?:[.!?)"”]|$))/im,
734
+ // "the league's own threshold, not a legal one"
735
+ /,\s*not\s+(?:a|an|the)\s+(?:[\w'’-]+\s+)?one\b/i,
736
+ // "renting computing over the internet instead of owning it"
737
+ /\b(?!(?:some|no|any|every)thing\b)\w{3,}ing\b[^.;:!?]{0,60}?\binstead\s+of\s+(?!(?:some|no|any|every)thing\b)\w{3,}ing\b/i
689
738
  ];
739
+ var PAIR_TOKEN = String.raw`(?:[£$€]?\d[\d,.]*%?|[\w'’£$€-]+)`;
740
+ var PAIR_NUMBER = String.raw`(?:one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|\d[\d,]*)`;
741
+ var PARALLEL_PAIR = [
742
+ // anaphora: "same site, same £8,000", "per user, per month". "No X, no Y" is no-x-no-y's.
743
+ new RegExp(String.raw`\b(?!no\b)(\w+)[ \t]+${PAIR_TOKEN},[ \t]+\1[ \t]+${PAIR_TOKEN}(?=[ \t]*(?:[.!?]|$))`, "im"),
744
+ // isocolon on its own line: "One form, seven workflows"
745
+ new RegExp(String.raw`^[ \t]*(?:#{1,6}[ \t]+)?${PAIR_NUMBER}[ \t]+[\w'’-]+,[ \t]+${PAIR_NUMBER}[ \t]+[\w'’-]+[.!]?[ \t]*$`, "im"),
746
+ // a verbless list as a tagline: "Service account, match-day reminders, analytics."
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
+ ];
749
+ var alreadyBlocked = (block, match, index) => {
750
+ const sentence = block.text.slice(Math.max(0, index - 80), index + match.length);
751
+ return NEGATION.some((pattern) => pattern.test(sentence));
752
+ };
690
753
  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;
691
754
  var SIGNED_IN = /(?:^|\/)(?:admin|portal|dashboard|embedded)\//;
692
755
  function lineAround(text, index) {
@@ -801,12 +864,25 @@ var COPY_TELLS = [
801
864
  generation: 2,
802
865
  severity: "warn",
803
866
  surface: "copy",
804
- why: "Told to avoid jargon, a model announces that it is avoiding jargon. 'Explained plainly' swaps a synonym and keeps the tell.",
805
- fix: "Use the house replacement, 'properly explained', or drop the claim and let the copy show it.",
806
- detect: phrases([/\b(?:plain english|explained plainly|plainly explained)\b/i], quoted),
867
+ why: "Told to avoid jargon, a model announces that it is avoiding jargon: 'in plain English', 'stated plainly', 'we'll tell you plainly', 'in plain terms'. The founder bans the whole family (20 October 2026); a reader takes it as filler.",
868
+ fix: "Drop the adverb and say the thing: 'Our prices, stated plainly' becomes 'Our prices'. Where the claim matters, the house replacement for 'plain English' is 'properly explained'.",
869
+ detect: phrases(PLAIN_FAMILY, quoted),
807
870
  fixtures: {
808
- flag: [f("content/home.md", "Your options, in plain English.")],
809
- pass: [f("content/home.md", "Your options, properly explained.")]
871
+ flag: [
872
+ f("content/home.md", "Your options, in plain English."),
873
+ // Eunice Ordor's draft lines, Pastel 986559 #12 and #13 (her diagnosis taken, her wording not).
874
+ f("content/clinics.md", "Tell us what's happening and we'll say plainly what we would take on."),
875
+ f("content/pricing.md", "DD's payment terms, plainly stated."),
876
+ f("content/clinics.md", "We explain our findings in plain terms."),
877
+ f("content/clinics.md", "We share findings in plain language.")
878
+ ],
879
+ pass: [
880
+ f("content/home.md", "Your options, properly explained."),
881
+ f("content/pricing.md", "Our payment terms: 30% when you sign, the rest at launch."),
882
+ // The Apache and zlib licence wording, copied into a repo as it is.
883
+ f("LICENSE.txt", "Altered source versions must be plainly marked as such."),
884
+ f("content/menu.md", "Plain or seeded bagels, and plain-language allergen notes.")
885
+ ]
810
886
  }
811
887
  },
812
888
  {
@@ -857,11 +933,11 @@ var COPY_TELLS = [
857
933
  generation: 1,
858
934
  severity: "warn",
859
935
  surface: "copy",
860
- why: "Truly, genuinely, clear, ultimately, 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking.",
936
+ why: "Truly, genuinely, clear, ultimately, 'a plain recommendation', 'the single most', 'bar none': normal English once, and a tell when they stack. The house rules keep them for review, never blocking.",
861
937
  fix: "Keep the one that lands hardest and cut the rest. 'What is genuinely excellent' reads better as 'what is excellent'.",
862
938
  detect: phrases([new RegExp(`(?:^|\\b)(?:${[...REVIEW_PHRASES].sort((a, b) => b.length - a.length).map((p) => escape(p).replace(/^(\w)/, "\\b$1").replace(/(\w)$/, "$1\\b")).join("|")})`, "i")], quoted),
863
939
  fixtures: {
864
- flag: [f("content/home.md", "We are truly proud of it."), f("content/home.md", "You leave with a clear next step.")],
940
+ flag: [f("content/home.md", "We are truly proud of it."), f("content/home.md", "You leave with a clear next step."), f("content/process.md", "You get a plain recommendation and someone who knows your systems.")],
865
941
  pass: [f("content/home.md", "We have fitted 400 boilers since 2009."), f("content/home.md", "Keep a safe clearance around the meter.")]
866
942
  }
867
943
  },
@@ -942,6 +1018,40 @@ var COPY_TELLS = [
942
1018
  ]
943
1019
  }
944
1020
  },
1021
+ {
1022
+ id: "confession-line",
1023
+ name: "Confession line",
1024
+ generation: 2,
1025
+ severity: "warn",
1026
+ surface: "copy",
1027
+ why: "'What this is and is not', 'we have not yet built', 'our experience so far is one practice', 'what we would and wouldn't take on': a model scopes a business by confessing its limits. It reads as small print, and it turns a buyer away before they make contact.",
1028
+ fix: "Scope by being precise about what is true, and leave the door open: 'Built and run by us for Sensphere, a practice whose clinicians work across several sites.' Keep contractual facts (eligibility, who runs what) and say them as what the buyer gets.",
1029
+ detect: phrases(CONFESSION, quoted),
1030
+ fixtures: {
1031
+ flag: [
1032
+ // Each one from the DD site, as Eunice Ordor flagged it (Pastel 986559 #8, #12, #13).
1033
+ f("content/clinics.md", "What this is and is not: a practice portal we built and run for one practice."),
1034
+ f("content/clinics.md", "We have not yet built and run the digital systems of a practice with several clinicians."),
1035
+ f("content/home.md", "Including the client portal we run for Sensphere. Our clinic experience so far is one practice."),
1036
+ f("content/clinics.md", "Tell us what's happening and we'll say what we would and wouldn't take on."),
1037
+ f("content/clinics.md", "We will say what carries over from this work and what we would need to learn first."),
1038
+ f("content/clinics.md", "We will tell you where our experience applies directly and where it is close."),
1039
+ f("content/about.md", "What we are not"),
1040
+ f("content/about.md", "Who we don't work with: anyone who needs a site this week."),
1041
+ f("content/mobile.md", "Mobile is our newest line, so we tell you what we have and haven't done.")
1042
+ ],
1043
+ pass: [
1044
+ f("content/clinics.md", "Built and run by us for Sensphere, a growing practice whose clinicians work remotely."),
1045
+ f("content/home.md", "The client portal we run for Sensphere."),
1046
+ // Contractual facts, said as they are.
1047
+ f("content/pricing.md", "Pay monthly is open to UK residents aged 18 and over, subject to status."),
1048
+ f("content/pricing.md", "Your monthly payments do not buy the site; the buy-out price does."),
1049
+ f("content/terms.md", "What we are responsible for, and what we are not responsible for, is in clause 4."),
1050
+ f("content/insurance.md", "Ask what your policy does and does not cover."),
1051
+ f("emails/deposit.md", "We have not yet received your deposit, so the date is still open.")
1052
+ ]
1053
+ }
1054
+ },
945
1055
  {
946
1056
  id: "hollow-imperative",
947
1057
  name: "Hollow imperative",
@@ -1020,7 +1130,7 @@ var COPY_TELLS = [
1020
1130
  generation: 2,
1021
1131
  severity: "warn",
1022
1132
  surface: "copy",
1023
- why: "'It's not just a haircut, it's an experience', 'dispatched from here, not shipped in': the contrast-and-reveal template is the most recognisable construction in generated copy.",
1133
+ why: "'It's not just a haircut, it's an experience', 'dispatched from here, not shipped in': the contrast-and-reveal template (correctio, antithesis: see COPY.md) is the most recognisable construction in generated copy.",
1024
1134
  fix: "Say the positive claim and drop the contrast.",
1025
1135
  detect: phrases(NEGATION, quoted, allText),
1026
1136
  fixtures: {
@@ -1031,12 +1141,54 @@ var COPY_TELLS = [
1031
1141
  f("content/home.md", "Hairdressing isn't a job, it's art."),
1032
1142
  f("content/home.md", "It is less about the tools and more about the eye."),
1033
1143
  f("content/home.md", "Order today and it's dispatched from here, not shipped in from Seoul."),
1034
- f("app/offer.tsx", `export const outcome = "The whole path, not just the ads.";`)
1144
+ f("app/offer.tsx", `export const outcome = "The whole path, not just the ads.";`),
1145
+ // From a 92EL report that passed the gate, October 2026.
1146
+ f("content/report.md", "These are reported, not assumed to pass."),
1147
+ f("content/report.md", "The deletion is proposed, not automated."),
1148
+ f("content/report.md", "For 92EL, that means renting Microsoft's services, not running servers."),
1149
+ f("content/home.md", "Your team should spend time delivering services, not managing bookings.")
1035
1150
  ],
1036
1151
  pass: [
1037
1152
  f("content/home.md", "A haircut, a hot towel and a straight-razor finish."),
1038
1153
  f("content/home.md", "They are cosmetics, not medicines."),
1039
- f("content/home.md", "The box arrived red, not led astray by a dented corner.")
1154
+ f("content/home.md", "The box arrived red, not led astray by a dented corner."),
1155
+ f("content/home.md", "We fix boilers in the morning, not in the evening."),
1156
+ f("content/report.md", "Players never open the lists."),
1157
+ f("content/report.md", "It is not tested at national scale."),
1158
+ f("content/report.md", "Do not invent anything."),
1159
+ f("content/report.md", "Nothing is copied by hand."),
1160
+ f("content/report.md", "Results are checked by a person and not yet automated.")
1161
+ ]
1162
+ }
1163
+ },
1164
+ {
1165
+ id: "trailing-contrast",
1166
+ name: "Trailing contrast",
1167
+ generation: 2,
1168
+ severity: "warn",
1169
+ surface: "copy",
1170
+ why: "'Edge matters for game servers, not registration', 'the league's own threshold, not a legal one', 'renting computing instead of owning it': the contrast template (correctio, antithesis: see COPY.md) in its softer forms. A genuine disclaimer has the same shape, so a person decides.",
1171
+ fix: "Say the positive claim on its own: 'Edge matters for game servers.' Keep the contrast only where the reader would otherwise assume the other thing, such as a legal disclaimer.",
1172
+ detect: phrases(SOFT_CONTRAST, quoted, allText, alreadyBlocked),
1173
+ fixtures: {
1174
+ flag: [
1175
+ // From a 92EL report that passed the gate, October 2026.
1176
+ f("content/report.md", "Edge matters for game servers, not registration."),
1177
+ f("content/report.md", "Age 18 is the league's own threshold, not a legal one."),
1178
+ 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.")
1180
+ ],
1181
+ pass: [
1182
+ f("content/home.md", "Rewire, retile and replumb a bathroom in five days."),
1183
+ f("content/report.md", "Players never open the lists."),
1184
+ f("content/report.md", "It is not tested at national scale."),
1185
+ f("content/report.md", "Do not invent anything."),
1186
+ f("content/report.md", "Nothing is copied by hand."),
1187
+ f("content/report.md", "Results are checked by a person and not yet automated."),
1188
+ f("content/home.md", "Pay by card, not to mention Apple Pay."),
1189
+ f("content/home.md", "Card instead of cash is fine."),
1190
+ // The blocking rule reports this one; it is not reported twice.
1191
+ f("content/report.md", "The deletion is proposed, not automated.")
1040
1192
  ]
1041
1193
  }
1042
1194
  },
@@ -1086,7 +1238,7 @@ var COPY_TELLS = [
1086
1238
  generation: 2,
1087
1239
  severity: "warn",
1088
1240
  surface: "copy",
1089
- why: "Three fragments in a row ('Fast. Friendly. Local.' or 'No fuss. No jargon. Just results.') is the second wave's favourite rhythm.",
1241
+ why: "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.",
1090
1242
  fix: "Write one sentence that says which of the three is true, and how you know.",
1091
1243
  // The fragments share one paragraph: "You don't. Again.", a blank line, then "It's frustrating." is two beats, not three.
1092
1244
  detect: phrases([/(?:\b[A-Z][\w'’-]*(?:[ \t]+[\w'’-]+){0,2}[.!](?:[ \t]+|[ \t]*\n(?![ \t]*\n)[ \t]*)){2}[A-Z][\w'’-]*(?:[ \t]+[\w'’-]+){0,2}[.!](?=\s|$)/], quoted),
@@ -1098,6 +1250,36 @@ var COPY_TELLS = [
1098
1250
  ]
1099
1251
  }
1100
1252
  },
1253
+ {
1254
+ id: "parallel-pair",
1255
+ name: "Verbless parallel pair",
1256
+ generation: 2,
1257
+ severity: "warn",
1258
+ surface: "copy",
1259
+ why: "'One form, seven workflows', 'Same site, same \xA38,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.",
1260
+ fix: "Write the sentence the pair stands for: 'One form and seven workflows replace the spreadsheets.' A short label on a slide can stay if a person reads it and it earns its place.",
1261
+ detect: phrases(PARALLEL_PAIR, quoted),
1262
+ fixtures: {
1263
+ flag: [
1264
+ // From 92EL deliverables, October 2026.
1265
+ f("content/deck.md", "# One form, seven workflows"),
1266
+ f("content/deck.md", "One form, seven workflows."),
1267
+ f("content/deck.md", "Same site, same \xA38,000."),
1268
+ f("content/deck.md", "Pay per user, per month"),
1269
+ f("content/deck.md", "Service account, match-day reminders, analytics.")
1270
+ ],
1271
+ pass: [
1272
+ f("content/deck.md", "Forms, SharePoint, Outlook and Teams"),
1273
+ f("content/deck.md", "One form and seven workflows replace 92EL's spreadsheets and email chains."),
1274
+ f("content/deck.md", "PulseCloud Technologies, for 92EL senior management"),
1275
+ f("content/deck.md", "41 of 50 passed, 12 after a fix."),
1276
+ f("content/deck.md", "Pay monthly for each user"),
1277
+ f("content/home.md", "We fix boilers the same day, and we tell you the price before we start."),
1278
+ // The blocking no-x-no-y reports this one.
1279
+ f("content/home.md", "We reply within a day. No obligation, no spam.")
1280
+ ]
1281
+ }
1282
+ },
1101
1283
  {
1102
1284
  id: "where-x-meets-y",
1103
1285
  name: "'Where X meets Y'",
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  snapshotUrl,
3
3
  snapshotUrls
4
- } from "./chunk-6DRBFDWT.js";
4
+ } from "./chunk-A7KUKNX6.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-6PZG4MJS.js";
8
+ } from "./chunk-3LHIKF5L.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-OVWVKMPS.js";
17
+ } from "./chunk-KGEJ2TGT.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-DE2ZAV4G.js";
31
+ } from "./chunk-3LCBLDAO.js";
32
32
 
33
33
  // src/audit/cli.ts
34
34
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -4,13 +4,13 @@ import {
4
4
  formatDirectionMatches,
5
5
  summariseDirection,
6
6
  validateDirection
7
- } from "./chunk-G47ZCBD5.js";
7
+ } from "./chunk-4NON4PVK.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-6PZG4MJS.js";
10
+ } from "./chunk-3LHIKF5L.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-OVWVKMPS.js";
13
+ } from "./chunk-KGEJ2TGT.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-DE2ZAV4G.js";
33
+ } from "./chunk-3LCBLDAO.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-LQ2TDISV.js");
136
+ const { snapshotUrl } = await import("./audit-CRTC7JSF.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(", ")}` : ""}`;
@@ -19,10 +19,10 @@ import {
19
19
  checkReason,
20
20
  researchPrompt,
21
21
  validateDirection
22
- } from "./chunk-G47ZCBD5.js";
22
+ } from "./chunk-4NON4PVK.js";
23
23
  import {
24
24
  parseFlags
25
- } from "./chunk-OVWVKMPS.js";
25
+ } from "./chunk-KGEJ2TGT.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-DE2ZAV4G.js";
42
- import "./chunk-ONA73THJ.js";
41
+ } from "./chunk-3LCBLDAO.js";
42
+ import "./chunk-KIBDFRHT.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-LQ2TDISV.js");
487
+ const { snapshotUrl } = await import("./audit-CRTC7JSF.js");
488
488
  return snapshotUrl(target);
489
489
  }
490
490
  return readSnapshot(readJson(resolve(cwd, target)), target);
@@ -9,13 +9,13 @@ import {
9
9
  runHarvest,
10
10
  runNull,
11
11
  serveDir
12
- } from "./chunk-6PZG4MJS.js";
12
+ } from "./chunk-3LHIKF5L.js";
13
13
  import "./chunk-OQXTQQAU.js";
14
- import "./chunk-OVWVKMPS.js";
14
+ import "./chunk-KGEJ2TGT.js";
15
15
  import "./chunk-QIEDBL4J.js";
16
16
  import "./chunk-WUACK2HX.js";
17
- import "./chunk-DE2ZAV4G.js";
18
- import "./chunk-ONA73THJ.js";
17
+ import "./chunk-3LCBLDAO.js";
18
+ import "./chunk-KIBDFRHT.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-ZS7PQNJU.js";
8
- import "./chunk-6DRBFDWT.js";
7
+ } from "./chunk-OTQQTXXK.js";
8
+ import "./chunk-A7KUKNX6.js";
9
9
  import "./chunk-JYNXGCYZ.js";
10
- import "./chunk-6PZG4MJS.js";
10
+ import "./chunk-3LHIKF5L.js";
11
11
  import "./chunk-OQXTQQAU.js";
12
- import "./chunk-OVWVKMPS.js";
12
+ import "./chunk-KGEJ2TGT.js";
13
13
  import "./chunk-QIEDBL4J.js";
14
14
  import "./chunk-WUACK2HX.js";
15
- import "./chunk-DE2ZAV4G.js";
16
- import "./chunk-ONA73THJ.js";
15
+ import "./chunk-3LCBLDAO.js";
16
+ import "./chunk-KIBDFRHT.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-OVWVKMPS.js";
19
+ } from "./chunk-KGEJ2TGT.js";
20
20
  import {
21
21
  toJson
22
22
  } from "./chunk-QIEDBL4J.js";
23
- import "./chunk-DE2ZAV4G.js";
24
- import "./chunk-ONA73THJ.js";
23
+ import "./chunk-3LCBLDAO.js";
24
+ import "./chunk-KIBDFRHT.js";
25
25
  import "./chunk-QYBAVU3P.js";
26
26
 
27
27
  // src/phone/cli.ts
@@ -4,11 +4,11 @@ import {
4
4
  import {
5
5
  expandNullTargets,
6
6
  loadNull
7
- } from "./chunk-6PZG4MJS.js";
7
+ } from "./chunk-3LHIKF5L.js";
8
8
  import "./chunk-OQXTQQAU.js";
9
9
  import {
10
10
  parseFlags
11
- } from "./chunk-OVWVKMPS.js";
11
+ } from "./chunk-KGEJ2TGT.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-DE2ZAV4G.js";
25
- import "./chunk-ONA73THJ.js";
24
+ } from "./chunk-3LCBLDAO.js";
25
+ import "./chunk-KIBDFRHT.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-LQ2TDISV.js");
191
+ const { snapshotUrls } = await import("./audit-CRTC7JSF.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];
@@ -3,21 +3,21 @@ import {
3
3
  } from "./chunk-OKVHWXYV.js";
4
4
  import {
5
5
  merge
6
- } from "./chunk-ZS7PQNJU.js";
7
- import "./chunk-6DRBFDWT.js";
6
+ } from "./chunk-OTQQTXXK.js";
7
+ import "./chunk-A7KUKNX6.js";
8
8
  import "./chunk-JYNXGCYZ.js";
9
9
  import {
10
10
  SIBLING_AT,
11
11
  compareToEstate,
12
12
  loadEstate
13
- } from "./chunk-RBQCGDQQ.js";
13
+ } from "./chunk-Y2LGYHJ5.js";
14
14
  import {
15
15
  PAGE_TYPES,
16
16
  validateDirection
17
- } from "./chunk-G47ZCBD5.js";
17
+ } from "./chunk-4NON4PVK.js";
18
18
  import {
19
19
  loadNull
20
- } from "./chunk-6PZG4MJS.js";
20
+ } from "./chunk-3LHIKF5L.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-OVWVKMPS.js";
31
+ } from "./chunk-KGEJ2TGT.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-DE2ZAV4G.js";
46
- import "./chunk-ONA73THJ.js";
45
+ } from "./chunk-3LCBLDAO.js";
46
+ import "./chunk-KIBDFRHT.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-LQ2TDISV.js");
129
+ const { snapshotUrl } = await import("./audit-CRTC7JSF.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-RBQCGDQQ.js";
8
- import "./chunk-G47ZCBD5.js";
9
- import "./chunk-6PZG4MJS.js";
7
+ } from "./chunk-Y2LGYHJ5.js";
8
+ import "./chunk-4NON4PVK.js";
9
+ import "./chunk-3LHIKF5L.js";
10
10
  import "./chunk-OQXTQQAU.js";
11
- import "./chunk-OVWVKMPS.js";
11
+ import "./chunk-KGEJ2TGT.js";
12
12
  import "./chunk-QIEDBL4J.js";
13
13
  import "./chunk-WUACK2HX.js";
14
- import "./chunk-DE2ZAV4G.js";
15
- import "./chunk-ONA73THJ.js";
14
+ import "./chunk-3LCBLDAO.js";
15
+ import "./chunk-KIBDFRHT.js";
16
16
  import "./chunk-QYBAVU3P.js";
17
17
  export {
18
18
  formatComponentPairs,
@@ -9,7 +9,7 @@ import {
9
9
  STOCK_PHRASES,
10
10
  countWords,
11
11
  specificity
12
- } from "./chunk-ONA73THJ.js";
12
+ } from "./chunk-KIBDFRHT.js";
13
13
 
14
14
  // src/register/cli.ts
15
15
  import { existsSync, readFileSync } from "fs";
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  run
4
- } from "./chunk-OVWVKMPS.js";
4
+ } from "./chunk-KGEJ2TGT.js";
5
5
  import "./chunk-QIEDBL4J.js";
6
- import "./chunk-DE2ZAV4G.js";
7
- import "./chunk-ONA73THJ.js";
6
+ import "./chunk-3LCBLDAO.js";
7
+ import "./chunk-KIBDFRHT.js";
8
8
  import "./chunk-QYBAVU3P.js";
9
9
 
10
10
  // src/cli.ts