@domandigital/craft 0.9.0 → 0.11.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.
@@ -0,0 +1,16 @@
1
+ import {
2
+ formatMatches,
3
+ formatPairs,
4
+ loadEstate,
5
+ runEstate
6
+ } from "./chunk-HOHHQB4N.js";
7
+ import "./chunk-3OZQ4C5R.js";
8
+ import "./chunk-2AJB27BJ.js";
9
+ import "./chunk-QMYERCBB.js";
10
+ import "./chunk-3NGWXZZE.js";
11
+ export {
12
+ formatMatches,
13
+ formatPairs,
14
+ loadEstate,
15
+ runEstate
16
+ };
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  run
4
- } from "./chunk-YLQE35PN.js";
5
- import "./chunk-ZT3WLN7R.js";
4
+ } from "./chunk-2AJB27BJ.js";
5
+ import "./chunk-3NGWXZZE.js";
6
6
 
7
7
  // src/cli.ts
8
8
  process.stdout.on("error", (error) => {
package/dist/index.cjs CHANGED
@@ -23,10 +23,12 @@ __export(index_exports, {
23
23
  AI_PHRASES: () => AI_PHRASES,
24
24
  AI_VIOLET: () => AI_VIOLET,
25
25
  AI_WORDS: () => AI_WORDS,
26
+ BASELINE_VERSION: () => BASELINE_VERSION,
26
27
  BUZZWORDS: () => BUZZWORDS,
27
28
  CATALOGUE: () => CATALOGUE,
28
29
  CATALOGUE_VERSION: () => CATALOGUE_VERSION,
29
30
  CHOICE_KEYS: () => CHOICE_KEYS,
31
+ DEFAULT_IGNORE: () => DEFAULT_IGNORE,
30
32
  DENSITIES: () => DENSITIES,
31
33
  DENSITY_NAMES: () => DENSITY_NAMES,
32
34
  DIRECTION_VERSION: () => DIRECTION_VERSION,
@@ -41,6 +43,7 @@ __export(index_exports, {
41
43
  HOUSE: () => HOUSE,
42
44
  HOUSE_BUDGET: () => HOUSE_BUDGET,
43
45
  HOUSE_TYPE: () => HOUSE_TYPE,
46
+ JSON_SCHEMA_VERSION: () => JSON_SCHEMA_VERSION,
44
47
  LIGHTNESS_CURVE: () => LIGHTNESS_CURVE,
45
48
  MIN_DECIDED: () => MIN_DECIDED,
46
49
  MIN_RUNS: () => MIN_RUNS,
@@ -71,7 +74,9 @@ __export(index_exports, {
71
74
  accentFork: () => accentFork,
72
75
  addSite: () => addSite,
73
76
  apcaContrast: () => apcaContrast,
77
+ applyBaseline: () => applyBaseline,
74
78
  applyHouseGate: () => applyHouseGate,
79
+ applySeverity: () => applySeverity,
75
80
  auditSnapshot: () => auditSnapshot,
76
81
  catalogueTable: () => catalogueTable,
77
82
  characterReport: () => characterReport,
@@ -82,6 +87,7 @@ __export(index_exports, {
82
87
  compareFacts: () => compareFacts,
83
88
  compareToEstate: () => compareToEstate,
84
89
  craftTokens: () => craftTokens,
90
+ createBaseline: () => createBaseline,
85
91
  deltaEOk: () => deltaEOk,
86
92
  deltaEOkHex: () => deltaEOkHex,
87
93
  densityCss: () => densityCss,
@@ -91,22 +97,27 @@ __export(index_exports, {
91
97
  estatePairs: () => estatePairs,
92
98
  exitDuration: () => exitDuration,
93
99
  extractHtml: () => extractHtml,
100
+ findClaims: () => findClaims,
94
101
  findColours: () => findColours,
102
+ findingKey: () => findingKey,
95
103
  fingerprint: () => fingerprint,
96
104
  fingerprintDistance: () => fingerprintDistance,
97
105
  fluidClamp: () => fluidClamp,
98
106
  fluidSpace: () => fluidSpace,
99
107
  fluidType: () => fluidType,
100
108
  fontClass: () => fontClass,
109
+ formatClaims: () => formatClaims,
101
110
  formatComparison: () => formatComparison,
102
111
  formatHex: () => formatHex,
103
112
  formatReport: () => formatReport,
113
+ globToRegExp: () => globToRegExp,
104
114
  groundName: () => groundName,
105
115
  harvest: () => harvest,
106
116
  hexToOklch: () => hexToOklch,
107
117
  houseRule: () => houseRule,
108
118
  hueName: () => hueName,
109
119
  hueSwatch: () => hueSwatch,
120
+ ignoreMatcher: () => ignoreMatcher,
110
121
  inP3Gamut: () => inP3Gamut,
111
122
  inSrgbGamut: () => inSrgbGamut,
112
123
  initDirection: () => initDirection,
@@ -114,13 +125,16 @@ __export(index_exports, {
114
125
  isCream: () => isCream,
115
126
  makeSnapshot: () => makeSnapshot,
116
127
  motionTokens: () => motionTokens,
128
+ namedSources: () => namedSources,
117
129
  nearest: () => nearest,
118
130
  normaliseFamily: () => normaliseFamily,
119
131
  nullPrompt: () => nullPrompt,
120
132
  oklchToHex: () => oklchToHex,
121
133
  oklchToRgb: () => oklchToRgb,
122
134
  paletteFromPixels: () => paletteFromPixels,
135
+ parseBaseline: () => parseBaseline,
123
136
  parseColour: () => parseColour,
137
+ parseCraftConfig: () => parseCraftConfig,
124
138
  parseHex: () => parseHex,
125
139
  propose: () => propose,
126
140
  protectedFacts: () => protectedFacts,
@@ -131,6 +145,7 @@ __export(index_exports, {
131
145
  scanSource: () => scanSource,
132
146
  sectionRhythm: () => sectionRhythm,
133
147
  semantic: () => semantic,
148
+ severityExceptions: () => severityExceptions,
134
149
  shapeName: () => shapeName,
135
150
  sharedParts: () => sharedParts,
136
151
  shouldAnimate: () => shouldAnimate,
@@ -139,6 +154,8 @@ __export(index_exports, {
139
154
  tailwindV4Theme: () => tailwindV4Theme,
140
155
  tellById: () => tellById,
141
156
  toGamut: () => toGamut,
157
+ toJson: () => toJson,
158
+ toSarif: () => toSarif,
142
159
  typeFeatureTokens: () => typeFeatureTokens,
143
160
  typicality: () => typicality,
144
161
  validateDirection: () => validateDirection,
@@ -1932,10 +1949,14 @@ var COPY_TELLS = [
1932
1949
  surface: "copy",
1933
1950
  why: "Three fragments in a row ('Fast. Friendly. Local.' or 'No fuss. No jargon. Just results.') is the second wave's favourite rhythm.",
1934
1951
  fix: "Write one sentence that says which of the three is true, and how you know.",
1935
- detect: phrases([/(?:\b[A-Z][\w'’-]*(?:\s+[\w'’-]+){0,2}[.!]\s+){2}[A-Z][\w'’-]*(?:\s+[\w'’-]+){0,2}[.!](?=\s|$)/], quoted),
1952
+ // The fragments share one paragraph: "You don't. Again.", a blank line, then "It's frustrating." is two beats, not three.
1953
+ 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),
1936
1954
  fixtures: {
1937
- flag: [f("content/home.md", "No fuss. No jargon. Just results.")],
1938
- pass: [f("content/home.md", "We fix boilers the same day, and we tell you the price before we start.")]
1955
+ flag: [f("content/home.md", "No fuss. No jargon. Just results."), f("content/home.md", "No fuss.\nNo jargon.\nJust results.")],
1956
+ pass: [
1957
+ f("content/home.md", "We fix boilers the same day, and we tell you the price before we start."),
1958
+ f("content/guide.md", "Your competitor appears in the top three. You don't. Again.\n\nIt's frustrating.")
1959
+ ]
1939
1960
  }
1940
1961
  },
1941
1962
  {
@@ -1954,6 +1975,14 @@ var COPY_TELLS = [
1954
1975
  }
1955
1976
  ];
1956
1977
  var ING_TAIL = /,\s+(?:ensuring|highlighting|underscoring|showcasing|reflecting|demonstrating|emphasi[sz]ing|fostering|cementing|solidifying|reinforcing|symboli[sz]ing|signalling|signaling)\b[^.!?\n]{0,80}/i;
1978
+ var PROMPT_CONTEXT = [
1979
+ /\b(?:the|this|that)\s+(?:attached|uploaded|provided)\s+(?:(?:Perplexity|ChatGPT|Claude|Gemini|Copilot)\s+)?(?:research|research file|research pack|source file|sources)\b/i,
1980
+ /\b(?:attached|uploaded)\s+(?:Perplexity|ChatGPT|Claude|Gemini|Copilot)\b/i,
1981
+ /\bthe\s+(?:attached|uploaded|provided)\s+brief\s+(?:asks|says|wants|requests|suggests|mentions)\b/i
1982
+ ];
1983
+ function gerundList(block, _match, index) {
1984
+ return /(?:^|[(,;:]\s*)\w+ing\b[^,.;()!?]{0,40}$/.test(block.text.slice(Math.max(0, index - 60), index));
1985
+ }
1957
1986
  var VAGUE_ATTRIBUTION = [
1958
1987
  /\b(?:studies|research|experts|scientists|surveys|statistics|the data)\s+(?:show|shows|suggest|suggests|agree|say|says|confirm|confirms|prove|proves|indicate|indicates)\b/i,
1959
1988
  /\b(?:many|most)\s+(?:experts|people|customers|professionals|homeowners)\s+(?:agree|say|believe)\b/i,
@@ -2039,8 +2068,10 @@ function inlineLabelHits(ctx) {
2039
2068
  run = [];
2040
2069
  };
2041
2070
  for (const line of lines) {
2042
- const m = /^\s*(?:[-*+]|\d+\.)\s+\*\*[^*]{1,40}:?\*\*:?\s+(\S.*)$/.exec(line);
2043
- if (m && m[1].trim().split(/\s+/).length <= 6) run.push(offset);
2071
+ const m = /^\s*[-*+]\s+\*\*([^*]{1,40}?):?\*\*:?\s+(\S.*)$/.exec(line);
2072
+ const data = m && /[\d£$€%`]|https?:|^\*\*/.test(m[2]);
2073
+ const named = m && /\S[A-Z]|[./]/.test(m[1]);
2074
+ if (m && !data && !named && m[2].trim().split(/\s+/).length <= 6) run.push(offset);
2044
2075
  else flush();
2045
2076
  offset += line.length + 1;
2046
2077
  }
@@ -2078,6 +2109,29 @@ var RESEARCH_TELLS = [
2078
2109
  ]
2079
2110
  }
2080
2111
  },
2112
+ {
2113
+ id: "prompt-context",
2114
+ name: "Prompt context",
2115
+ generation: 1,
2116
+ severity: "warn",
2117
+ surface: "copy",
2118
+ why: "'This article uses the attached Perplexity research file as the primary source', 'the attached research is right to say': the model talking about what it was given. A reader of a web page has no attachment. Found 38 times across five live DD articles in 2026-09, none caught by any other tell.",
2119
+ fix: "Delete the reference to the research or brief. Where it carried a claim, name the real source or cut the claim.",
2120
+ detect: phrases(PROMPT_CONTEXT, quoted),
2121
+ fixtures: {
2122
+ flag: [
2123
+ f("content/guide.md", "This article uses the attached Perplexity research file as the primary source."),
2124
+ f("content/guide.md", "The attached research is right to say these platforms suit early-stage businesses."),
2125
+ f("content/guide.md", "That is exactly how the uploaded research frames the category."),
2126
+ f("content/guide.md", "The attached brief asks for the point that most tradespeople miss.")
2127
+ ],
2128
+ pass: [
2129
+ f("content/guide.md", "We research every quote before we send it."),
2130
+ f("app/email.tsx", `export const Invoice = () => <p>Your invoice is attached.</p>;`),
2131
+ f("content/guide.md", "BrightLocal's research shows that reviews drive 20% of local ranking.")
2132
+ ]
2133
+ }
2134
+ },
2081
2135
  {
2082
2136
  id: "placeholder",
2083
2137
  name: "Unfilled placeholder",
@@ -2130,7 +2184,11 @@ var RESEARCH_TELLS = [
2130
2184
  pass: [
2131
2185
  f("content/home.md", "- Pages load in under a second\n- Card details never touch our server\n- Support by phone until 8pm\n"),
2132
2186
  f("content/home.md", "- **Speed:** Faster pages\n- **Security:** Safer data\n"),
2133
- f("content/guide.md", "- **GDC**: The statutory regulator for all dental professionals.\n- **GCC**: The statutory regulator for chiropractors in the UK.\n- **CQC**: The independent regulator of health and social care in England.\n")
2187
+ f("content/guide.md", "- **GDC**: The statutory regulator for all dental professionals.\n- **GCC**: The statutory regulator for chiropractors in the UK.\n- **CQC**: The independent regulator of health and social care in England.\n"),
2188
+ f("content/guide.md", "- **Per page**: \xA3100-\xA3200 per page\n- **Hourly**: \xA350-\xA375/hour\n- **Day rate**: \xA3350-\xA3500\n"),
2189
+ f("content/guide.md", "- **Law Society Find a Solicitor:** For solicitors\n- **AccountingWEB / ICAEW directories:** For accountants\n- **NHS.uk:** For NHS services\n"),
2190
+ f("content/guide.md", "1. **Visit PageSpeed Insights** and paste the address\n2. **Enter their URL** and run it\n3. **Note their scores** for later\n"),
2191
+ f("content/guide.md", "- **Structural problems:** **REBUILD.**\n- **Messaging issues:** **REFRESH.**\n- **Neither:** **LEAVE IT.**\n")
2134
2192
  ]
2135
2193
  }
2136
2194
  },
@@ -2142,7 +2200,7 @@ var RESEARCH_TELLS = [
2142
2200
  surface: "copy",
2143
2201
  why: "'..., ensuring peace of mind', '..., highlighting our commitment': a participle tacked on the end that claims significance and names no mechanism. One of the most common shapes in the Wikipedia guide to AI writing.",
2144
2202
  fix: "Cut the tail, or replace it with the mechanism: 'so you can drop the car off before work'.",
2145
- detect: phrases([ING_TAIL], (m) => quoted(m.replace(/^,\s*/, ""))),
2203
+ detect: phrases([ING_TAIL], (m) => quoted(m.replace(/^,\s*/, "")), void 0, gerundList),
2146
2204
  fixtures: {
2147
2205
  flag: [
2148
2206
  f("content/home.md", "Every engineer is Gas Safe registered, ensuring complete peace of mind."),
@@ -2150,7 +2208,8 @@ var RESEARCH_TELLS = [
2150
2208
  ],
2151
2209
  pass: [
2152
2210
  f("content/home.md", "We open at 7am, so you can drop the car off before work."),
2153
- f("content/home.md", "Engineers arrive by 9, carrying the parts for most boilers.")
2211
+ f("content/home.md", "Engineers arrive by 9, carrying the parts for most boilers."),
2212
+ f("content/guide.md", "Many items (claiming your GBP, ensuring NAP consistency, writing title tags) are straightforward.")
2154
2213
  ]
2155
2214
  }
2156
2215
  },
@@ -2400,6 +2459,7 @@ function repeatHits(ctx) {
2400
2459
  for (const s of sentences(doc2)) {
2401
2460
  const words2 = normal(s.text);
2402
2461
  if (words2.length < RUN || !/[.!?]["'’”)]*$/.test(s.text)) continue;
2462
+ if (/^\s*\[\^[\w-]+\]:/.test(s.text)) continue;
2403
2463
  const key = words2.join(" ");
2404
2464
  const earlier = whole.get(key);
2405
2465
  if (earlier) {
@@ -2597,7 +2657,15 @@ var DENSITY_TELLS = [
2597
2657
  f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nMore here.\n\nWe check every valve before the engineer leaves the house.")),
2598
2658
  f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nBy then we check every valve before the engineer leaves the site."))
2599
2659
  ],
2600
- pass: [f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nThe engineer leaves once the customer has signed the sheet."))]
2660
+ pass: [
2661
+ f2("content/proposal.md", doc("We check every valve before the engineer leaves the house.\n\nThe engineer leaves once the customer has signed the sheet.")),
2662
+ f2(
2663
+ "content/guide.md",
2664
+ doc(
2665
+ "The Independent Healthcare Providers Network Private Patient Insight Report 2024 found that private demand rose.[^1]\n\n[^1]: Independent Healthcare Providers Network, Private Patient Insight Report 2024, published online."
2666
+ )
2667
+ )
2668
+ ]
2601
2669
  }
2602
2670
  },
2603
2671
  {
@@ -3634,6 +3702,9 @@ var HOUSE = {
3634
3702
  "closing-summary": { tier: "review", label: "closing summary" },
3635
3703
  "false-range": { tier: "review", label: "false range" },
3636
3704
  "placeholder": { tier: "review", label: "unfilled placeholder" },
3705
+ // Every estate hit so far is real (38 in five DD articles, 2026-09-24), but
3706
+ // a proposal can honestly say "the attached research". Review until measured there.
3707
+ "prompt-context": { tier: "review", label: "prompt context: the model naming its own inputs" },
3637
3708
  "question-reveal": { tier: "review", label: "staged reveal" },
3638
3709
  "inline-label-list": { tier: "review", label: "bold-label bullets" },
3639
3710
  "phrase-density": { tier: "review", label: "density" },
@@ -4316,7 +4387,7 @@ var RENDERED_PATHS = {
4316
4387
  };
4317
4388
 
4318
4389
  // src/character/check.ts
4319
- var CATALOGUE_VERSION = "2026.09.7";
4390
+ var CATALOGUE_VERSION = "2026.09.8";
4320
4391
  var CATALOGUE = [...SOURCE_TELLS, ...COPY_TELLS, ...RESEARCH_TELLS, ...DENSITY_TELLS].map(
4321
4392
  (t) => RENDERED_PATHS[t.id] ? { ...t, rendered: RENDERED_PATHS[t.id] } : t
4322
4393
  );
@@ -4581,6 +4652,308 @@ function formatComparison(result, beforePath, afterPath) {
4581
4652
  return lines.join("\n");
4582
4653
  }
4583
4654
 
4655
+ // src/character/claims.ts
4656
+ var CLAIM_KINDS = /* @__PURE__ */ new Set(["money", "number", "date"]);
4657
+ var PROPER = "[A-Z][\\w&.\u2019'-]*(?:\\s+(?:of|and|&|the|for)?\\s*[A-Z][\\w&.\u2019'-]*)*";
4658
+ var NOT_A_SOURCE = /^(?:We|I|It|This|That|These|Those|They|He|She|You|Our|Your|The|A|An|Each|Every|Most|Many|Some|One|Research|Studies|Experts|Data|Analysis|If|When|So|But|And|Or|In|On|At|From|For|By|As|Since|After|Before|Across|Over|Under|Its|Their|External|Source|UK|US|EU|Sometimes|Public|Recent|Industry|Independent|What|How|Why|Who|Where|Businesses|Customers|Clients|Patients|Consumers|People|Users|Online)$/;
4659
+ var NOUN = "study|survey|report|research|analysis|benchmark|benchmarks|guidelines|docs|documentation|help centre|help center|pricing page|figures|statistics|case study";
4660
+ var OWNED_NOUN = "guide|audit|support|pricing|data|site|website|blog";
4661
+ var VERB = "found|finds|reports?|reported|says|said|states?|stated|estimates?|estimated|recommends?|publishes|published|advertises|advertised|shows|showed|claims";
4662
+ var ATTRIBUTION = [
4663
+ new RegExp(`\\b[Aa]ccording to\\s+(?:the\\s+)?(${PROPER})`, "g"),
4664
+ new RegExp(`\\b(?:${NOUN})\\s+(?:by|from|commissioned by)\\s+(?:the\\s+)?(${PROPER})`, "g"),
4665
+ // "Akamai’s 2017 retail study", "the 2020 Deloitte study", "Bark’s UK guide", "Wix’s own developer guidelines".
4666
+ new RegExp(`\\b(${PROPER})(?:(?:\u2019s|'s)\\s+(?:own\\s+)?(?:\\d{4}\\s+)?(?:[A-Za-z]+\\s+)?(?:${NOUN}|${OWNED_NOUN})|\\s+(?:\\d{4}\\s+)?(?:[a-z]+\\s+)?(?:${NOUN}))\\b`, "g"),
4667
+ // "Google also reported", "Shopify UK currently advertises".
4668
+ new RegExp(`\\b(${PROPER})\\s+(?:also\\s+|now\\s+|currently\\s+|still\\s+)?(?:${VERB})\\b`, "g"),
4669
+ /\((?:[Ss]ources?:\s*|via\s+(?=[A-Z]))([^)]+)\)/g,
4670
+ /^\s*(?:source|sources)\s*:\s*(.+)$/gim
4671
+ ];
4672
+ var LINK2 = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|\bhttps?:\/\/[^\s<>"')\]]+/g;
4673
+ function cleanName(raw) {
4674
+ const words2 = raw.trim().replace(/[.,;:]+$/, "").split(/\s+/);
4675
+ while (words2.length && NOT_A_SOURCE.test(words2[0])) words2.shift();
4676
+ return words2.join(" ").replace(/(?:’s|'s)(?=\s|$).*$/, "").trim();
4677
+ }
4678
+ var IMPERATIVE = /^(?:Add|Use|Check|Make|Get|Set|Include|Create|Update|List|Keep|Ask|Write|Track|Test|Claim|Build|Link|Submit|Verify|Review|Fix|Remove|Upload|Respond|Reply|Choose|Pick|Start|Run|Open|Install|Connect|Enable|Turn|Put|Give|Send|Book|Call|Read|See|Try|Compare|Aim|Target|Avoid|Post|Share|Mark|Tag)$/;
4679
+ var atStart = (sentence, index) => /^[\s*_"“”’(>#|-]*(?:\d{1,3}[.)]\s*)?[\s*_]*(?:[\w ]+:\**\s*)?$/.test(sentence.slice(0, index)) || /\|\s*\**$/.test(sentence.slice(0, index));
4680
+ function namedSources(sentence) {
4681
+ const found = /* @__PURE__ */ new Set();
4682
+ ATTRIBUTION.forEach((pattern, i) => {
4683
+ for (const m of sentence.matchAll(pattern)) {
4684
+ const raw = m[1].trim();
4685
+ const word = !/\s/.test(raw) && !/[’']s$/.test(m[0].split(/\s+/)[0]);
4686
+ if (i === 2 && word && atStart(sentence, m.index ?? 0)) continue;
4687
+ if (IMPERATIVE.test(raw.split(/\s+/)[0]) && atStart(sentence, m.index ?? 0)) continue;
4688
+ const name = cleanName(raw);
4689
+ if (!name || name.length < 2 || !/\s/.test(name) && /[a-z]ing$/.test(name)) continue;
4690
+ found.add(name);
4691
+ }
4692
+ });
4693
+ for (const m of sentence.matchAll(LINK2)) found.add(m[2] ?? m[0]);
4694
+ return [...found];
4695
+ }
4696
+ var FOOTNOTE_REF = /\[\^([\w-]+)\](?!:)/g;
4697
+ var FOOTNOTE_DEF = /^[ \t]*\[\^([\w-]+)\]:[ \t]*(.+)$/gm;
4698
+ var ASSUMPTION = /^(?:\*\*)?(?:assume|suppose|imagine|say|let's say|for example, say|picture)\b/i;
4699
+ var LEAD = /^\s*(?:#{1,6}\s+|>\s*|[-*+]\s+|\d+[.)]\s+)*/;
4700
+ function sentences2(text) {
4701
+ const out = [];
4702
+ let start = 0;
4703
+ let paragraph = 0;
4704
+ const push = (end) => {
4705
+ const raw = text.slice(start, end);
4706
+ const lead = raw.length - raw.trimStart().length;
4707
+ if (raw.trim()) out.push({ text: raw.trim(), offset: start + lead, paragraph });
4708
+ };
4709
+ for (const m of text.matchAll(/(?<!(?:^|\n)[ \t]*\d{1,3})[.!?](?:\[\^[\w-]+\])*(?=["”’)]?[ \t]+["“(*]*[A-Z0-9£$€])|\n+/g)) {
4710
+ const end = (m.index ?? 0) + m[0].length;
4711
+ push(end);
4712
+ start = end;
4713
+ if (/\n\s*\n/.test(m[0]) || /^\s*(?:#|\|)/.test(text.slice(end))) paragraph += 1;
4714
+ }
4715
+ push(text.length);
4716
+ return out;
4717
+ }
4718
+ var MAX = 180;
4719
+ var trim = (s) => s.length > MAX ? `${s.slice(0, MAX - 1).trimEnd()}\u2026` : s;
4720
+ function findClaims(files) {
4721
+ const claims = [];
4722
+ for (const file of files) {
4723
+ for (const block of extractCopy(file)) {
4724
+ const notes = new Map([...block.text.matchAll(FOOTNOTE_DEF)].map((m) => [m[1], m[2].trim()]));
4725
+ let carried;
4726
+ for (const s of sentences2(block.text)) {
4727
+ if (/^\s*#{1,6}\s/.test(s.text) || /^\s*\[\^[\w-]+\]:/.test(s.text)) continue;
4728
+ const refs = [...s.text.matchAll(FOOTNOTE_REF)].map((m) => notes.get(m[1]) ?? `footnote ${m[1]}`);
4729
+ const text = s.text.replace(FOOTNOTE_REF, "");
4730
+ const body = text.replace(LEAD, "");
4731
+ const own = [...namedSources(text), ...refs];
4732
+ if (own.length) carried = { paragraph: s.paragraph, sources: own };
4733
+ if (ASSUMPTION.test(body)) continue;
4734
+ const facts = protectedFacts(body).filter((f4) => CLAIM_KINDS.has(f4.kind));
4735
+ if (facts.length === 0 && own.length === 0) continue;
4736
+ const nearby = own.length === 0 && carried?.paragraph === s.paragraph ? carried.sources : void 0;
4737
+ const at = block.offset + s.offset;
4738
+ claims.push({
4739
+ path: file.path,
4740
+ line: file.text.slice(0, at).split("\n").length,
4741
+ sentence: trim(text.replace(/\s+/g, " ")),
4742
+ facts,
4743
+ sources: own,
4744
+ ...nearby ? { nearby } : {}
4745
+ });
4746
+ }
4747
+ }
4748
+ }
4749
+ return { files: files.length, claims, unsourced: claims.filter((c) => c.sources.length === 0).length };
4750
+ }
4751
+ function formatClaims(report2, label = "craft copy claims") {
4752
+ const { claims, unsourced } = report2;
4753
+ if (claims.length === 0) return `${label}: no prices, figures, dates or named sources in ${report2.files} file${report2.files === 1 ? "" : "s"}.`;
4754
+ const lines = [
4755
+ `${label}: ${claims.length} claim${claims.length === 1 ? "" : "s"} to check, ${unsourced} with no source named.`,
4756
+ " Check each against its primary source before it ships. Sourced means a source is named, not that it agrees."
4757
+ ];
4758
+ for (const c of claims) {
4759
+ const what = [
4760
+ ...c.facts.map((f4) => f4.value),
4761
+ ...c.sources.map((s) => `source: ${s}`),
4762
+ ...c.nearby ? [`${c.nearby.join(", ")} named earlier in the paragraph: does it cover this?`] : []
4763
+ ].join(" \xB7 ");
4764
+ lines.push("", ` ${c.path}:${c.line} ${c.sources.length ? "sourced " : "UNSOURCED"} ${what}`, ` ${c.sentence}`);
4765
+ }
4766
+ return lines.join("\n");
4767
+ }
4768
+
4769
+ // src/character/baseline.ts
4770
+ var BASELINE_VERSION = 1;
4771
+ function fnv1a(text) {
4772
+ let hash = 2166136261;
4773
+ for (let i = 0; i < text.length; i += 1) {
4774
+ hash ^= text.charCodeAt(i);
4775
+ hash = Math.imul(hash, 16777619);
4776
+ }
4777
+ return (hash >>> 0).toString(16).padStart(8, "0");
4778
+ }
4779
+ var excerptHash = (f4) => fnv1a(f4.excerpt.replace(/\s+/g, " ").trim());
4780
+ var keyOf = (tell, path, excerpt) => `${tell}\0${path}\0${excerpt}`;
4781
+ function findingKey(f4) {
4782
+ return keyOf(f4.tell, f4.path, excerptHash(f4));
4783
+ }
4784
+ function createBaseline(report2) {
4785
+ const counts = /* @__PURE__ */ new Map();
4786
+ for (const f4 of report2.findings) {
4787
+ const excerpt = excerptHash(f4);
4788
+ const key = keyOf(f4.tell, f4.path, excerpt);
4789
+ const entry = counts.get(key);
4790
+ if (entry) entry.count += 1;
4791
+ else counts.set(key, { tell: f4.tell, path: f4.path, excerpt, count: 1 });
4792
+ }
4793
+ const entries = [...counts.values()].sort((a, b) => a.path.localeCompare(b.path) || a.tell.localeCompare(b.tell) || a.excerpt.localeCompare(b.excerpt));
4794
+ return { version: BASELINE_VERSION, catalogueVersion: report2.catalogueVersion, entries };
4795
+ }
4796
+ function parseBaseline(data, source = "baseline") {
4797
+ const b = data;
4798
+ if (!b || typeof b !== "object" || b.version !== BASELINE_VERSION || !Array.isArray(b.entries)) {
4799
+ throw new Error(`${source}: not a craft baseline (version ${BASELINE_VERSION})`);
4800
+ }
4801
+ for (const e of b.entries) {
4802
+ if (typeof e?.tell !== "string" || typeof e.path !== "string" || typeof e.excerpt !== "string" || !Number.isInteger(e.count) || e.count < 1) {
4803
+ throw new Error(`${source}: malformed entry ${JSON.stringify(e)}`);
4804
+ }
4805
+ }
4806
+ return b;
4807
+ }
4808
+ function recount(report2, findings) {
4809
+ const byGeneration = { 1: 0, 2: 0, 3: 0 };
4810
+ const byTell = {};
4811
+ for (const f4 of findings) {
4812
+ byGeneration[f4.generation] += 1;
4813
+ byTell[f4.tell] = (byTell[f4.tell] ?? 0) + 1;
4814
+ }
4815
+ return {
4816
+ ...report2,
4817
+ findings,
4818
+ summary: { ...report2.summary, findings: findings.length, byGeneration, byTell, blocking: findings.filter((f4) => f4.severity === "block").length }
4819
+ };
4820
+ }
4821
+ function applyBaseline(report2, baseline) {
4822
+ const remaining = /* @__PURE__ */ new Map();
4823
+ for (const e of baseline.entries) {
4824
+ const key = keyOf(e.tell, e.path, e.excerpt);
4825
+ remaining.set(key, (remaining.get(key) ?? 0) + e.count);
4826
+ }
4827
+ const kept = [];
4828
+ let baselined = 0;
4829
+ for (const f4 of report2.findings) {
4830
+ const key = findingKey(f4);
4831
+ const left = remaining.get(key) ?? 0;
4832
+ if (left > 0) {
4833
+ remaining.set(key, left - 1);
4834
+ baselined += 1;
4835
+ } else kept.push(f4);
4836
+ }
4837
+ return { ...recount(report2, kept), baselined };
4838
+ }
4839
+
4840
+ // src/character/config.ts
4841
+ var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
4842
+ var KEYS = /* @__PURE__ */ new Set(["$schema", "ignore", "copyPaths", "severity"]);
4843
+ var LEVELS = /* @__PURE__ */ new Set(["warn", "block", "off"]);
4844
+ var MIN_BECAUSE = 12;
4845
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4846
+ var isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === "string");
4847
+ function parseCraftConfig(data, knownTells, source = "craft.config.json") {
4848
+ if (!isRecord(data)) throw new Error(`${source}: expected an object`);
4849
+ for (const key of Object.keys(data)) if (!KEYS.has(key)) throw new Error(`${source}: unknown key "${key}"`);
4850
+ const ignore = data.ignore ?? [];
4851
+ if (!isStringArray(ignore)) throw new Error(`${source}: "ignore" must be an array of glob strings`);
4852
+ if (data.copyPaths !== void 0 && !isStringArray(data.copyPaths)) throw new Error(`${source}: "copyPaths" must be an array of paths`);
4853
+ const severity = {};
4854
+ const raw = data.severity ?? {};
4855
+ if (!isRecord(raw)) throw new Error(`${source}: "severity" must map a tell id to { level, because }`);
4856
+ for (const [tell, value] of Object.entries(raw)) {
4857
+ if (!knownTells.has(tell)) throw new Error(`${source}: severity names no tell called "${tell}"`);
4858
+ if (!isRecord(value) || !LEVELS.has(value.level)) {
4859
+ throw new Error(`${source}: severity.${tell} needs a level of "warn", "block" or "off"`);
4860
+ }
4861
+ if (typeof value.because !== "string" || value.because.trim().length < MIN_BECAUSE) {
4862
+ throw new Error(`${source}: severity.${tell} needs a because of at least a sentence`);
4863
+ }
4864
+ severity[tell] = { level: value.level, because: value.because };
4865
+ }
4866
+ return { ignore, severity, ...data.copyPaths ? { copyPaths: data.copyPaths } : {} };
4867
+ }
4868
+ function globToRegExp(glob) {
4869
+ const pattern = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
4870
+ let body = "";
4871
+ for (let i = 0; i < pattern.length; i += 1) {
4872
+ const c = pattern[i];
4873
+ if (c === "*" && pattern[i + 1] === "*") {
4874
+ const slash = pattern[i + 2] === "/";
4875
+ body += slash ? "(?:.*/)?" : ".*";
4876
+ i += slash ? 2 : 1;
4877
+ } else if (c === "*") body += "[^/]*";
4878
+ else if (c === "?") body += "[^/]";
4879
+ else body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
4880
+ }
4881
+ const anywhere = !pattern.includes("/");
4882
+ return new RegExp(`^${anywhere ? "(?:.*/)?" : ""}${body}(?:/.*)?$`);
4883
+ }
4884
+ function ignoreMatcher(patterns) {
4885
+ const res = patterns.map(globToRegExp);
4886
+ return (path) => {
4887
+ const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
4888
+ return res.some((re) => re.test(p));
4889
+ };
4890
+ }
4891
+ function severityExceptions(config) {
4892
+ return Object.entries(config.severity).filter(([, o]) => o.level === "off").map(([tell, o]) => ({ tell, because: `craft.config.json: ${o.because}` }));
4893
+ }
4894
+ function applySeverity(report2, config) {
4895
+ const findings = report2.findings.map((f4) => {
4896
+ const o = config.severity[f4.tell];
4897
+ return o && o.level !== "off" ? { ...f4, severity: o.level } : f4;
4898
+ });
4899
+ return { ...report2, findings, summary: { ...report2.summary, blocking: findings.filter((f4) => f4.severity === "block").length } };
4900
+ }
4901
+
4902
+ // src/character/sarif.ts
4903
+ var isUrl = (path) => /^[a-z][a-z0-9+.-]*:\/\//i.test(path);
4904
+ function toSarif(report2, options) {
4905
+ const byId = new Map(options.tells.map((t) => [t.id, t]));
4906
+ const ids = [...new Set(report2.findings.map((f4) => f4.tell))].sort();
4907
+ const index = new Map(ids.map((id, i) => [id, i]));
4908
+ const rules = ids.map((id) => {
4909
+ const t = byId.get(id);
4910
+ return {
4911
+ id,
4912
+ name: t?.name ?? id,
4913
+ shortDescription: { text: t?.name ?? id },
4914
+ ...t ? { fullDescription: { text: t.why }, help: { text: t.fix } } : {}
4915
+ };
4916
+ });
4917
+ const results = report2.findings.map((f4) => ({
4918
+ ruleId: f4.tell,
4919
+ ruleIndex: index.get(f4.tell),
4920
+ level: f4.severity === "block" ? "error" : "warning",
4921
+ message: { text: `${f4.message}. ${f4.fix}` },
4922
+ locations: [
4923
+ {
4924
+ physicalLocation: {
4925
+ artifactLocation: { uri: isUrl(f4.path) ? f4.path : f4.path.replace(/\\/g, "/") },
4926
+ ...f4.line > 0 ? { region: { startLine: f4.line, snippet: { text: f4.excerpt } } } : {}
4927
+ }
4928
+ }
4929
+ ],
4930
+ partialFingerprints: { "craftFinding/v1": fnv1a(findingKey(f4)) }
4931
+ }));
4932
+ return {
4933
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
4934
+ version: "2.1.0",
4935
+ runs: [
4936
+ {
4937
+ tool: {
4938
+ driver: {
4939
+ name: "craft",
4940
+ version: report2.catalogueVersion,
4941
+ informationUri: options.informationUri ?? "https://www.npmjs.com/package/@domandigital/craft",
4942
+ rules
4943
+ }
4944
+ },
4945
+ results
4946
+ }
4947
+ ]
4948
+ };
4949
+ }
4950
+
4951
+ // src/character/json.ts
4952
+ var JSON_SCHEMA_VERSION = 1;
4953
+ function toJson(data) {
4954
+ return JSON.stringify({ schemaVersion: JSON_SCHEMA_VERSION, ...data }, null, 2);
4955
+ }
4956
+
4584
4957
  // src/fingerprint/index.ts
4585
4958
  var FINGERPRINT_VERSION = 1;
4586
4959
  var CHROMATIC = 0.03;
@@ -5476,10 +5849,12 @@ function retrofitPlan(report2, direction) {
5476
5849
  AI_PHRASES,
5477
5850
  AI_VIOLET,
5478
5851
  AI_WORDS,
5852
+ BASELINE_VERSION,
5479
5853
  BUZZWORDS,
5480
5854
  CATALOGUE,
5481
5855
  CATALOGUE_VERSION,
5482
5856
  CHOICE_KEYS,
5857
+ DEFAULT_IGNORE,
5483
5858
  DENSITIES,
5484
5859
  DENSITY_NAMES,
5485
5860
  DIRECTION_VERSION,
@@ -5494,6 +5869,7 @@ function retrofitPlan(report2, direction) {
5494
5869
  HOUSE,
5495
5870
  HOUSE_BUDGET,
5496
5871
  HOUSE_TYPE,
5872
+ JSON_SCHEMA_VERSION,
5497
5873
  LIGHTNESS_CURVE,
5498
5874
  MIN_DECIDED,
5499
5875
  MIN_RUNS,
@@ -5524,7 +5900,9 @@ function retrofitPlan(report2, direction) {
5524
5900
  accentFork,
5525
5901
  addSite,
5526
5902
  apcaContrast,
5903
+ applyBaseline,
5527
5904
  applyHouseGate,
5905
+ applySeverity,
5528
5906
  auditSnapshot,
5529
5907
  catalogueTable,
5530
5908
  characterReport,
@@ -5535,6 +5913,7 @@ function retrofitPlan(report2, direction) {
5535
5913
  compareFacts,
5536
5914
  compareToEstate,
5537
5915
  craftTokens,
5916
+ createBaseline,
5538
5917
  deltaEOk,
5539
5918
  deltaEOkHex,
5540
5919
  densityCss,
@@ -5544,22 +5923,27 @@ function retrofitPlan(report2, direction) {
5544
5923
  estatePairs,
5545
5924
  exitDuration,
5546
5925
  extractHtml,
5926
+ findClaims,
5547
5927
  findColours,
5928
+ findingKey,
5548
5929
  fingerprint,
5549
5930
  fingerprintDistance,
5550
5931
  fluidClamp,
5551
5932
  fluidSpace,
5552
5933
  fluidType,
5553
5934
  fontClass,
5935
+ formatClaims,
5554
5936
  formatComparison,
5555
5937
  formatHex,
5556
5938
  formatReport,
5939
+ globToRegExp,
5557
5940
  groundName,
5558
5941
  harvest,
5559
5942
  hexToOklch,
5560
5943
  houseRule,
5561
5944
  hueName,
5562
5945
  hueSwatch,
5946
+ ignoreMatcher,
5563
5947
  inP3Gamut,
5564
5948
  inSrgbGamut,
5565
5949
  initDirection,
@@ -5567,13 +5951,16 @@ function retrofitPlan(report2, direction) {
5567
5951
  isCream,
5568
5952
  makeSnapshot,
5569
5953
  motionTokens,
5954
+ namedSources,
5570
5955
  nearest,
5571
5956
  normaliseFamily,
5572
5957
  nullPrompt,
5573
5958
  oklchToHex,
5574
5959
  oklchToRgb,
5575
5960
  paletteFromPixels,
5961
+ parseBaseline,
5576
5962
  parseColour,
5963
+ parseCraftConfig,
5577
5964
  parseHex,
5578
5965
  propose,
5579
5966
  protectedFacts,
@@ -5584,6 +5971,7 @@ function retrofitPlan(report2, direction) {
5584
5971
  scanSource,
5585
5972
  sectionRhythm,
5586
5973
  semantic,
5974
+ severityExceptions,
5587
5975
  shapeName,
5588
5976
  sharedParts,
5589
5977
  shouldAnimate,
@@ -5592,6 +5980,8 @@ function retrofitPlan(report2, direction) {
5592
5980
  tailwindV4Theme,
5593
5981
  tellById,
5594
5982
  toGamut,
5983
+ toJson,
5984
+ toSarif,
5595
5985
  typeFeatureTokens,
5596
5986
  typicality,
5597
5987
  validateDirection,