@tangle-network/agent-app 0.60.211 → 0.60.212
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.
|
@@ -109,7 +109,13 @@ var PHRASE_RULES = [
|
|
|
109
109
|
/\bthe (?:result|catch|best part|secret|kicker|twist|answer|difference|outcome|upshot)\?\s+\S/i
|
|
110
110
|
),
|
|
111
111
|
rule("colon-reveal", "Write it as a normal sentence.")(
|
|
112
|
-
|
|
112
|
+
/(?:\b(?:the best part|here'?s the (?:secret|thing|kicker|catch)|one word|the result|the truth|the secret)\s*:\s)|(?:(?:^|[.!?]\s+)(?:[\w']+'s\s+)?(?:the\s+|our\s+|my\s+|here's\s+the\s+|the\s+real\s+)?(?:answer|catch|twist|kicker|truth|secret|takeaway|bottom line|short version|good news|upshot|lesson|difference|correction|short intro|intro|question|problem|idea)\s*:\s+\S|(?:^|[.!?]\s+)the [\w-]+ that [^.:\n]{3,50}:\s+\S)/im
|
|
113
|
+
),
|
|
114
|
+
rule("contrast-reveal", "State the real point directly with its reason; do not stage a denial first.")(
|
|
115
|
+
/\bmost [\w\s-]{1,40}\b(?:are|is)\b[^.]{1,60}\.\s+(?:the|but the)\s+(?:unsolved|hard|real|missing|interesting)\s+(?:part|problem)\b|\btakes a middle (?:position|path|ground)\b|\bthe first question is\b/i
|
|
116
|
+
),
|
|
117
|
+
rule("opener-closer", "End on the question or the point.")(
|
|
118
|
+
/\bone-line (?:reply|answer) (?:is )?(?:welcome|fine)\b|\beither answer helps\b|\beither way,? thanks\b|\bthanks for reading\b|\bno worries if not\b|\bquick question\b/i
|
|
113
119
|
),
|
|
114
120
|
rule("hype-punctuation", "Remove the stacked punctuation.")(/[!?]{2,}/),
|
|
115
121
|
rule("decoration", "Remove decorative unicode letters and emoji bullets.")(
|
|
@@ -186,6 +192,7 @@ function scanCopy(text, options = {}) {
|
|
|
186
192
|
}
|
|
187
193
|
let contrast = false;
|
|
188
194
|
for (const { pattern, category, fix } of PHRASE_RULES) {
|
|
195
|
+
if ((surface === "changelog" || surface === "show-hn") && (category === "colon-reveal" || category === "contrast-reveal")) continue;
|
|
189
196
|
const match = paragraph.match(pattern);
|
|
190
197
|
if (!match) continue;
|
|
191
198
|
if (category === "contrast-reveal") {
|
|
@@ -263,4 +270,4 @@ export {
|
|
|
263
270
|
metricClaims,
|
|
264
271
|
unsourcedMetrics
|
|
265
272
|
};
|
|
266
|
-
//# sourceMappingURL=chunk-
|
|
273
|
+
//# sourceMappingURL=chunk-K6D5EGN2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/copy-quality/index.ts"],"sourcesContent":["/**\n * `@tangle-network/agent-app/copy-quality` — deterministic checks for copy an\n * agent writes for a reader: ads, posts, subject lines, emails, landing pages.\n *\n * No model call. A scan returns findings an agent can fix before a person sees\n * the draft, and the same findings feed a product's outcome checks, so the\n * in-run feedback and the scored episode never disagree.\n *\n * Tiers follow the \"AI tells\" list in coreyhaines31/marketingskills (MIT,\n * revision f719a8079c694e3267d47b6b60a62aa926055f2c,\n * skills/copywriting/references/ai-tells.md):\n * - ban: a tell on one sighting.\n * - cap: fine once; a tell when two land in one paragraph.\n * - weak: reported, never failing on its own.\n * Weighting follows blader/humanizer (MIT, revision\n * 225a6f39ac85f76ee48dbad772ea4abe4ed6c9d8): staged contrasts, negation lists\n * and staged reveals count on one sighting; a dash or a curly quote in long\n * copy is weak alone. The density score mirrors the EQ-Bench Slop Score mix:\n * 60% tell-word rate, 25% contrast constructions, 15% repeated trigrams.\n *\n * Code blocks, inline code and URLs are removed before scanning: an SDK\n * snippet in a post is the product, not prose.\n */\n\nimport { canonicalizeValue, valuesInText } from '../work-product/claim-support'\n\nexport type CopySurface = 'short' | 'long' | 'changelog' | 'show-hn'\n\nexport type CopyTellTier = 'ban' | 'cap' | 'weak'\n\nexport type CopyTellCategory =\n | 'banned-word'\n | 'cap-words'\n | 'contrast-reveal'\n | 'negation-list'\n | 'question-reveal'\n | 'colon-reveal'\n | 'opener-closer'\n | 'marketing-phrase'\n | 'puffery'\n | 'weasel-source'\n | 'chummy-opener'\n | 'hype-punctuation'\n | 'decoration'\n | 'fragment-drumbeat'\n | 'dash'\n | 'chat-wrapper'\n\nexport interface CopyTellFinding {\n tier: CopyTellTier\n category: CopyTellCategory\n /** The text that triggered the finding. */\n match: string\n /** Zero-based paragraph index, or -1 for a whole-text finding. */\n paragraph: number\n /** What to do instead, written for the agent that will fix it. */\n fix: string\n}\n\nexport interface CopyScanOptions {\n /**\n * `short` = headline, subject line, ad, social post, SMS, hero, CTA: no\n * dashes at all. `long` = article, email body, page: two dashes allowed.\n * `changelog` and `show-hn` use long-copy punctuation and exempt reveal constructions.\n * Default `long`.\n */\n surface?: CopySurface\n /** Product-specific banned words or phrases (case-insensitive, whole word). */\n extraBanned?: readonly string[]\n}\n\nexport interface CopyScanResult {\n /** No ban-tier and no cap-tier findings. Weak findings never fail alone. */\n pass: boolean\n findings: CopyTellFinding[]\n counts: Record<CopyTellTier, number>\n /** 0 (clean) to 100 (dense with tells). Informational; `pass` is the gate. */\n slopScore: number\n words: number\n}\n\n/**\n * Ban-tier vocabulary: ai-tells.md \"Vocabulary → Ban\". Base forms; the scanner\n * also matches inflections (\"leveraging\", \"synergies\") and a hyphen written as\n * a space (\"game changer\"), so prompts can list each word once.\n */\nexport const BAN_WORDS: readonly string[] = [\n 'delve', 'tapestry', 'testament to', 'realm', 'embark', 'beacon', 'multifaceted',\n 'paradigm', 'synergy', 'myriad', 'plethora', 'meticulous', 'intricate', 'utilize',\n 'leverage', 'supercharge', 'turbocharge', 'game-changer', 'game-changing',\n 'ever-evolving landscape', 'plays a crucial role',\n]\n\n/** Cap-tier vocabulary: ai-tells.md \"Vocabulary → Cap\" plus empty intensifiers. Base forms. */\nexport const CAP_WORDS: readonly string[] = [\n 'seamless', 'robust', 'streamline', 'empower', 'elevate', 'unlock', 'unleash',\n 'harness', 'foster', 'enhance', 'optimize', 'cutting-edge', 'innovative',\n 'revolutionary', 'transformative', 'holistic', 'comprehensive', 'pivotal', 'crucial',\n 'vital', 'powerful', 'next-level', 'world-class', 'best-in-class', 'landscape',\n 'navigate', 'ecosystem', 'journey',\n 'truly', 'genuinely', 'incredibly', 'deeply', 'fundamentally', 'significantly',\n 'simply', 'actually', 'literally', 'quietly',\n]\n\ninterface PhraseRule {\n readonly pattern: RegExp\n readonly category: CopyTellCategory\n readonly fix: string\n}\n\nconst rule = (category: CopyTellCategory, fix: string) => (pattern: RegExp): PhraseRule => ({ pattern, category, fix })\n\nconst CONTRAST_FIX = 'State the real point directly with its reason; do not stage a denial first.'\n\nconst PHRASE_RULES: readonly PhraseRule[] = [\n ...[\n /\\bin today'?s (?:fast-paced |digital |modern )?(?:world|landscape|market|age)\\b/i,\n /\\bwhether you'?re an? [^,.]{1,40},? or an? /i,\n /(?:^|[.!?]\\s+)imagine\\b/i,\n /\\bwhen it comes to\\b/i,\n /\\bhere'?s the thing\\b|\\bthe truth is\\b|\\blet me be clear\\b|\\bhere'?s what nobody tells you\\b/i,\n /\\bin this (?:guide|post|article),? we'?ll\\b|\\blet'?s dive in(?:to)?\\b/i,\n /\\bin conclusion\\b|\\bat the end of the day\\b/i,\n /(?:^|[.!?]\\s+)(?:it'?s worth noting|moreover|additionally|importantly|ultimately)\\b/i,\n ].map(rule('opener-closer', 'Delete the wind-up and start with the point.')),\n ...[\n /\\bsay (?:goodbye|hello) to\\b/i,\n /\\b(?:unlock|unleash) the (?:full )?power of\\b/i,\n /\\b\\w+, (?:reimagined|redefined)\\b/i,\n /\\bto the next level\\b/i,\n /\\btransform the way you\\b/i,\n /\\bthe \\w+ you deserve\\b/i,\n /\\bthe future of [\\w ]{1,30} is here\\b/i,\n /\\bbuilt for teams (?:who|that) move fast\\b/i,\n /\\beverything you need to\\b|\\ball-in-one\\b/i,\n /\\beffortless(?:ly)?\\b|\\bin just a few clicks\\b/i,\n /\\bjoin (?:thousands|millions|hundreds) of\\b/i,\n /\\bchanges everything\\b/i,\n /\\bexperience the power of\\b/i,\n ].map(rule('marketing-phrase', 'Replace with the concrete outcome, number or workflow.')),\n rule('puffery', 'State the fact; let the reader judge it.')(/\\b(?:marks|represents) a pivotal moment\\b|\\bredefines the category\\b/i),\n rule('weasel-source', 'Name the source or the customer, or cut the claim.')(/\\bexperts agree\\b|\\bstudies show\\b|\\btrusted by (?:industry )?leaders\\b/i),\n rule('chummy-opener', 'Cut the chummy opener.')(/^(?:great question|let'?s be honest|we get it)\\b/im),\n rule('contrast-reveal', CONTRAST_FIX)(\n /\\bit'?s not (?:just )?[^.,;!?]{1,60}[,;.]\\s*it'?s\\b|\\b(?:this|that) isn'?t [^.;!?]{1,60}\\.\\s*(?:this|that|it) is\\b|\\bnot just [^.;!?]{1,60},? but\\b|\\bnot because [^.;!?]{1,60}\\.\\s*because\\b|\\bless [a-z]+, more [a-z]+\\b|\\bthe question isn'?t\\b|\\?\\s*think again\\b/i,\n ),\n rule('contrast-reveal', CONTRAST_FIX)(/(?:^|[.!?]\\s+)Not [^.;!?]{1,40}\\.\\s+[A-Z][^.;!?]{1,60}\\./),\n rule('negation-list', 'Say what does happen. One plain \"No card required\" beside the CTA is fine.')(\n /\\bno [a-z-]+(?: [a-z-]+)?[,.]\\s*no [a-z-]+/i,\n ),\n rule('question-reveal', 'Delete the question and keep the answer.')(\n /\\bthe (?:result|catch|best part|secret|kicker|twist|answer|difference|outcome|upshot)\\?\\s+\\S/i,\n ),\n rule('colon-reveal', 'Write it as a normal sentence.')(\n /(?:\\b(?:the best part|here'?s the (?:secret|thing|kicker|catch)|one word|the result|the truth|the secret)\\s*:\\s)|(?:(?:^|[.!?]\\s+)(?:[\\w']+'s\\s+)?(?:the\\s+|our\\s+|my\\s+|here's\\s+the\\s+|the\\s+real\\s+)?(?:answer|catch|twist|kicker|truth|secret|takeaway|bottom line|short version|good news|upshot|lesson|difference|correction|short intro|intro|question|problem|idea)\\s*:\\s+\\S|(?:^|[.!?]\\s+)the [\\w-]+ that [^.:\\n]{3,50}:\\s+\\S)/im,\n ),\n rule('contrast-reveal', 'State the real point directly with its reason; do not stage a denial first.')(\n /\\bmost [\\w\\s-]{1,40}\\b(?:are|is)\\b[^.]{1,60}\\.\\s+(?:the|but the)\\s+(?:unsolved|hard|real|missing|interesting)\\s+(?:part|problem)\\b|\\btakes a middle (?:position|path|ground)\\b|\\bthe first question is\\b/i,\n ),\n rule('opener-closer', 'End on the question or the point.')(\n /\\bone-line (?:reply|answer) (?:is )?(?:welcome|fine)\\b|\\beither answer helps\\b|\\beither way,? thanks\\b|\\bthanks for reading\\b|\\bno worries if not\\b|\\bquick question\\b/i,\n ),\n rule('hype-punctuation', 'Remove the stacked punctuation.')(/[!?]{2,}/),\n rule('decoration', 'Remove decorative unicode letters and emoji bullets.')(\n /[\\u{1D400}-\\u{1D7FF}]|^\\s*(?:[-*•]\\s*)?[\\u{1F300}-\\u{1FAFF}\\u{2600}-\\u{27BF}]/mu,\n ),\n]\n\nconst CHAT_WRAPPERS: readonly RegExp[] = [\n /^\\s*(?:sure|certainly|absolutely|of course|great)[!,.]/i,\n /^\\s*here(?:'s| is| are) (?:a|an|the|your|my) (?:draft|version|post|email|ad|headline|copy|subject|options?|revised)/i,\n /\\bi hope (?:this|that) helps\\b/i,\n /\\blet me know if you(?:'d like| want| need)\\b/i,\n /\\b(?:want|would you like) me to\\b/i,\n /\\bfeel free to (?:adjust|tweak|edit)\\b/i,\n]\n\n// An em dash anywhere; an en dash, double hyphen or single hyphen used as a\n// dash between words. A range like 10–20 has no spaces and is not a dash.\nconst DASH = /—|(?<=\\S)[ \\t]+(?:–|--?)[ \\t]+(?=\\S)/g\nconst CURLY = /[‘’“”]/\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** Inflections of one word: \"leverage\" → leverages, leveraged, leveraging. */\nfunction inflected(word: string): string {\n const stem = escapeRegExp(word)\n if (/e$/.test(word)) return `${stem.slice(0, -1)}(?:e|es|ed|ing|ely)`\n if (/[^aeiou]y$/.test(word)) return `${stem.slice(0, -1)}(?:y|ies|ied|ying|ily)`\n return `${stem}(?:s|es|ed|ing|ly)?`\n}\n\n/**\n * A whole-word matcher for a term. A single word (hyphenated or not) also\n * matches its inflections; a phrase matches as written. Hyphens and spaces in\n * a term match either, and a space matches any run of whitespace.\n */\nfunction termRegex(term: string): RegExp {\n const body = /\\s/.test(term.trim())\n ? escapeRegExp(term.trim())\n : inflected(term.trim())\n return new RegExp(`(?<![\\\\w-])${body.replace(/-|\\s+/g, '[-\\\\s]+')}(?![\\\\w-])`, 'gi')\n}\n\nconst BAN_REGEXES = BAN_WORDS.map((word) => ({ word, regex: termRegex(word) }))\nconst CAP_REGEXES = CAP_WORDS.map((word) => ({ word, regex: termRegex(word) }))\n\n/** The prose a reader reads: code, URLs and quote styles normalized away. */\nexport function proseOf(text: string): string {\n return text\n .replace(/```[\\s\\S]*?```/g, '\\n\\n')\n .replace(/`[^`\\n]*`/g, 'code')\n .replace(/https?:\\/\\/\\S+/g, 'link')\n .replace(/[‘’]/g, \"'\")\n .replace(/[“”]/g, '\"')\n}\n\nfunction paragraphsOf(prose: string): string[] {\n return prose.split(/\\n\\s*\\n/).map((p) => p.trim()).filter(Boolean)\n}\n\nfunction wordCount(text: string): number {\n return (text.match(/[A-Za-z0-9][\\w'-]*/g) ?? []).length\n}\n\nfunction repeatedTrigramRate(prose: string): number {\n const words = prose.toLowerCase().match(/[a-z0-9][a-z0-9'-]*/g) ?? []\n if (words.length < 12) return 0\n const seen = new Map<string, number>()\n for (let i = 0; i + 2 < words.length; i += 1) {\n const key = `${words[i]} ${words[i + 1]} ${words[i + 2]}`\n seen.set(key, (seen.get(key) ?? 0) + 1)\n }\n let repeats = 0\n for (const count of seen.values()) if (count > 1) repeats += count - 1\n return repeats / (words.length - 2)\n}\n\n/** Scan copy for AI tells. Deterministic and cheap; run it on every draft. */\nexport function scanCopy(text: string, options: CopyScanOptions = {}): CopyScanResult {\n const surface = options.surface ?? 'long'\n const prose = proseOf(text)\n const paragraphs = paragraphsOf(prose)\n // An extra already in the Ban tier (or listed twice) must not count twice.\n const extras = [...new Set((options.extraBanned ?? []).map((word) => word.toLowerCase()))]\n .filter((word) => !BAN_WORDS.includes(word))\n const banned = [...BAN_REGEXES, ...extras.map((word) => ({ word, regex: termRegex(word) }))]\n // A product that bans a cap-tier word outright gets one finding for it, not two.\n const capped = CAP_REGEXES.filter(({ word }) => !extras.includes(word))\n const findings: CopyTellFinding[] = []\n let contrastParagraphs = 0\n let tellWords = 0\n\n paragraphs.forEach((paragraph, index) => {\n for (const { word, regex } of banned) {\n for (const match of paragraph.matchAll(regex)) {\n tellWords += 1\n findings.push({ tier: 'ban', category: 'banned-word', match: match[0], paragraph: index, fix: `Replace \"${word}\" with the fact it stands in for.` })\n }\n }\n const capMatches = capped.flatMap(({ regex }) => [...paragraph.matchAll(regex)].map((match) => match[0]))\n tellWords += capMatches.length\n if (capMatches.length >= 2) {\n findings.push({ tier: 'cap', category: 'cap-words', match: capMatches.join(', '), paragraph: index, fix: 'Two cap-tier words in one paragraph: rewrite it from the facts.' })\n }\n let contrast = false\n for (const { pattern, category, fix } of PHRASE_RULES) {\n if ((surface === 'changelog' || surface === 'show-hn') && (category === 'colon-reveal' || category === 'contrast-reveal')) continue\n const match = paragraph.match(pattern)\n if (!match) continue\n if (category === 'contrast-reveal') {\n if (contrast) continue\n contrast = true\n }\n findings.push({ tier: 'ban', category, match: match[0].trim(), paragraph: index, fix })\n }\n if (contrast) contrastParagraphs += 1\n let run = 0\n for (const sentence of paragraph.split(/(?<=[.!?])\\s+/)) {\n const count = wordCount(sentence)\n run = count > 0 && count <= 3 ? run + 1 : 0\n if (run === 3) {\n findings.push({ tier: 'cap', category: 'fragment-drumbeat', match: sentence, paragraph: index, fix: 'Merge the fragments into one sentence that carries a fact.' })\n break\n }\n }\n })\n\n const dashes = prose.match(DASH)?.length ?? 0\n if (dashes > 0) {\n const tier: CopyTellTier = surface === 'short' ? 'ban' : dashes > 2 ? 'cap' : 'weak'\n const fix = surface === 'short'\n ? 'Short copy uses no dashes between words: use a period, comma or colon.'\n : 'Long copy allows two dashes per page at most; prefer a period or comma.'\n findings.push({ tier, category: 'dash', match: `${dashes} dash${dashes === 1 ? '' : 'es'}`, paragraph: -1, fix })\n }\n if (CURLY.test(text)) {\n findings.push({ tier: 'weak', category: 'decoration', match: 'curly quotes', paragraph: -1, fix: 'Weak alone: use the quote style the target format uses.' })\n }\n const wrapper = chatWrapperIn(text)\n if (wrapper !== null) {\n findings.push({ tier: 'ban', category: 'chat-wrapper', match: wrapper, paragraph: -1, fix: 'Return only the deliverable, with no chat around it.' })\n }\n\n const counts: Record<CopyTellTier, number> = { ban: 0, cap: 0, weak: 0 }\n for (const finding of findings) counts[finding.tier] += 1\n const words = wordCount(prose)\n // Saturates at 40 tell words per 1,000, a contrast in every paragraph, and 10% repeated trigrams.\n const wordRate = Math.min(1, (tellWords * 1000) / Math.max(1, words) / 40)\n const contrastRate = paragraphs.length === 0 ? 0 : contrastParagraphs / paragraphs.length\n const trigramRate = Math.min(1, repeatedTrigramRate(prose) * 10)\n const slopScore = Math.round(100 * (0.6 * wordRate + 0.25 * contrastRate + 0.15 * trigramRate))\n return { pass: counts.ban === 0 && counts.cap === 0, findings, counts, slopScore, words }\n}\n\n/** The chat wrapper around a deliverable (\"Here's a draft…\", \"Hope this helps\"), or null. */\nexport function chatWrapperIn(text: string): string | null {\n const prose = proseOf(text)\n for (const pattern of CHAT_WRAPPERS) {\n const match = prose.match(pattern)\n if (match) return match[0].trim()\n }\n return null\n}\n\n/**\n * Banned words and phrases for prompts that list them: base forms, lowercased,\n * deduplicated, product extras first. The scanner matches their inflections.\n */\nexport function bannedCopyVocabulary(extra: readonly string[] = []): string[] {\n return [...new Set([...extra, ...BAN_WORDS].map((word) => word.toLowerCase()))]\n}\n\nexport interface MetricClaim {\n /** The figure as written, e.g. \"$0.02/hr\", \"40%\", \"10x\", \"300ms\". */\n text: string\n /** Canonical value used to match it against sources (claim-support rules). */\n value: string\n}\n\n// A figure a reader takes as a measured fact: money, a percentage, a multiple,\n// a duration or size with its unit, a scaled count, or a grouped number.\n// Bare small integers (\"3 steps\") and years are not metrics.\nconst METRIC =\n /[$€£¥₹]\\s?\\d[\\d,]*(?:\\.\\d+)?(?:\\s?[kKmMbB]\\b)?|\\b\\d[\\d,]*(?:\\.\\d+)?\\s?(?:%|x\\b|×|ms\\b|s\\b|sec\\b|secs\\b|seconds?\\b|mins?\\b|minutes?\\b|hrs?\\b|hours?\\b|days?\\b|[kKmMbB]\\+?(?=\\s|$|[.,;])|[KMGT]B\\b|rps\\b|qps\\b|tokens?\\b)|\\b\\d{1,3}(?:,\\d{3})+(?:\\.\\d+)?\\b/g\n\n/** Every figure in `text` a reader would take as a measured fact. */\nexport function metricClaims(text: string): MetricClaim[] {\n const claims: MetricClaim[] = []\n for (const match of proseOf(text).matchAll(METRIC)) {\n const numeric = match[0].match(/[$€£¥₹]?\\s?\\d[\\d,]*(?:\\.\\d+)?/)?.[0]\n const value = numeric ? canonicalizeValue(numeric) : null\n if (value !== null) claims.push({ text: match[0].trim(), value })\n }\n return claims\n}\n\n/**\n * Metric claims in `copy` whose value appears in none of `sources`: figures\n * the writer cannot point to. Values match with claim-support's\n * canonicalization, so \"$1,200\" in copy matches \"1200.00\" in a source.\n */\nexport function unsourcedMetrics(copy: string, sources: readonly string[]): MetricClaim[] {\n const known = new Set(sources.flatMap((source) => valuesInText(source)))\n return metricClaims(copy).filter((claim) => !known.has(claim.value))\n}\n"],"mappings":"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","names":[]}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Code blocks, inline code and URLs are removed before scanning: an SDK
|
|
22
22
|
* snippet in a post is the product, not prose.
|
|
23
23
|
*/
|
|
24
|
-
export type CopySurface = 'short' | 'long';
|
|
24
|
+
export type CopySurface = 'short' | 'long' | 'changelog' | 'show-hn';
|
|
25
25
|
export type CopyTellTier = 'ban' | 'cap' | 'weak';
|
|
26
26
|
export type CopyTellCategory = 'banned-word' | 'cap-words' | 'contrast-reveal' | 'negation-list' | 'question-reveal' | 'colon-reveal' | 'opener-closer' | 'marketing-phrase' | 'puffery' | 'weasel-source' | 'chummy-opener' | 'hype-punctuation' | 'decoration' | 'fragment-drumbeat' | 'dash' | 'chat-wrapper';
|
|
27
27
|
export interface CopyTellFinding {
|
|
@@ -38,6 +38,7 @@ export interface CopyScanOptions {
|
|
|
38
38
|
/**
|
|
39
39
|
* `short` = headline, subject line, ad, social post, SMS, hero, CTA: no
|
|
40
40
|
* dashes at all. `long` = article, email body, page: two dashes allowed.
|
|
41
|
+
* `changelog` and `show-hn` use long-copy punctuation and exempt reveal constructions.
|
|
41
42
|
* Default `long`.
|
|
42
43
|
*/
|
|
43
44
|
surface?: CopySurface;
|
package/dist/profile/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/copy-quality/index.ts"],"sourcesContent":["/**\n * `@tangle-network/agent-app/copy-quality` — deterministic checks for copy an\n * agent writes for a reader: ads, posts, subject lines, emails, landing pages.\n *\n * No model call. A scan returns findings an agent can fix before a person sees\n * the draft, and the same findings feed a product's outcome checks, so the\n * in-run feedback and the scored episode never disagree.\n *\n * Tiers follow the \"AI tells\" list in coreyhaines31/marketingskills (MIT,\n * revision f719a8079c694e3267d47b6b60a62aa926055f2c,\n * skills/copywriting/references/ai-tells.md):\n * - ban: a tell on one sighting.\n * - cap: fine once; a tell when two land in one paragraph.\n * - weak: reported, never failing on its own.\n * Weighting follows blader/humanizer (MIT, revision\n * 225a6f39ac85f76ee48dbad772ea4abe4ed6c9d8): staged contrasts, negation lists\n * and staged reveals count on one sighting; a dash or a curly quote in long\n * copy is weak alone. The density score mirrors the EQ-Bench Slop Score mix:\n * 60% tell-word rate, 25% contrast constructions, 15% repeated trigrams.\n *\n * Code blocks, inline code and URLs are removed before scanning: an SDK\n * snippet in a post is the product, not prose.\n */\n\nimport { canonicalizeValue, valuesInText } from '../work-product/claim-support'\n\nexport type CopySurface = 'short' | 'long'\n\nexport type CopyTellTier = 'ban' | 'cap' | 'weak'\n\nexport type CopyTellCategory =\n | 'banned-word'\n | 'cap-words'\n | 'contrast-reveal'\n | 'negation-list'\n | 'question-reveal'\n | 'colon-reveal'\n | 'opener-closer'\n | 'marketing-phrase'\n | 'puffery'\n | 'weasel-source'\n | 'chummy-opener'\n | 'hype-punctuation'\n | 'decoration'\n | 'fragment-drumbeat'\n | 'dash'\n | 'chat-wrapper'\n\nexport interface CopyTellFinding {\n tier: CopyTellTier\n category: CopyTellCategory\n /** The text that triggered the finding. */\n match: string\n /** Zero-based paragraph index, or -1 for a whole-text finding. */\n paragraph: number\n /** What to do instead, written for the agent that will fix it. */\n fix: string\n}\n\nexport interface CopyScanOptions {\n /**\n * `short` = headline, subject line, ad, social post, SMS, hero, CTA: no\n * dashes at all. `long` = article, email body, page: two dashes allowed.\n * Default `long`.\n */\n surface?: CopySurface\n /** Product-specific banned words or phrases (case-insensitive, whole word). */\n extraBanned?: readonly string[]\n}\n\nexport interface CopyScanResult {\n /** No ban-tier and no cap-tier findings. Weak findings never fail alone. */\n pass: boolean\n findings: CopyTellFinding[]\n counts: Record<CopyTellTier, number>\n /** 0 (clean) to 100 (dense with tells). Informational; `pass` is the gate. */\n slopScore: number\n words: number\n}\n\n/**\n * Ban-tier vocabulary: ai-tells.md \"Vocabulary → Ban\". Base forms; the scanner\n * also matches inflections (\"leveraging\", \"synergies\") and a hyphen written as\n * a space (\"game changer\"), so prompts can list each word once.\n */\nexport const BAN_WORDS: readonly string[] = [\n 'delve', 'tapestry', 'testament to', 'realm', 'embark', 'beacon', 'multifaceted',\n 'paradigm', 'synergy', 'myriad', 'plethora', 'meticulous', 'intricate', 'utilize',\n 'leverage', 'supercharge', 'turbocharge', 'game-changer', 'game-changing',\n 'ever-evolving landscape', 'plays a crucial role',\n]\n\n/** Cap-tier vocabulary: ai-tells.md \"Vocabulary → Cap\" plus empty intensifiers. Base forms. */\nexport const CAP_WORDS: readonly string[] = [\n 'seamless', 'robust', 'streamline', 'empower', 'elevate', 'unlock', 'unleash',\n 'harness', 'foster', 'enhance', 'optimize', 'cutting-edge', 'innovative',\n 'revolutionary', 'transformative', 'holistic', 'comprehensive', 'pivotal', 'crucial',\n 'vital', 'powerful', 'next-level', 'world-class', 'best-in-class', 'landscape',\n 'navigate', 'ecosystem', 'journey',\n 'truly', 'genuinely', 'incredibly', 'deeply', 'fundamentally', 'significantly',\n 'simply', 'actually', 'literally', 'quietly',\n]\n\ninterface PhraseRule {\n readonly pattern: RegExp\n readonly category: CopyTellCategory\n readonly fix: string\n}\n\nconst rule = (category: CopyTellCategory, fix: string) => (pattern: RegExp): PhraseRule => ({ pattern, category, fix })\n\nconst CONTRAST_FIX = 'State the real point directly with its reason; do not stage a denial first.'\n\nconst PHRASE_RULES: readonly PhraseRule[] = [\n ...[\n /\\bin today'?s (?:fast-paced |digital |modern )?(?:world|landscape|market|age)\\b/i,\n /\\bwhether you'?re an? [^,.]{1,40},? or an? /i,\n /(?:^|[.!?]\\s+)imagine\\b/i,\n /\\bwhen it comes to\\b/i,\n /\\bhere'?s the thing\\b|\\bthe truth is\\b|\\blet me be clear\\b|\\bhere'?s what nobody tells you\\b/i,\n /\\bin this (?:guide|post|article),? we'?ll\\b|\\blet'?s dive in(?:to)?\\b/i,\n /\\bin conclusion\\b|\\bat the end of the day\\b/i,\n /(?:^|[.!?]\\s+)(?:it'?s worth noting|moreover|additionally|importantly|ultimately)\\b/i,\n ].map(rule('opener-closer', 'Delete the wind-up and start with the point.')),\n ...[\n /\\bsay (?:goodbye|hello) to\\b/i,\n /\\b(?:unlock|unleash) the (?:full )?power of\\b/i,\n /\\b\\w+, (?:reimagined|redefined)\\b/i,\n /\\bto the next level\\b/i,\n /\\btransform the way you\\b/i,\n /\\bthe \\w+ you deserve\\b/i,\n /\\bthe future of [\\w ]{1,30} is here\\b/i,\n /\\bbuilt for teams (?:who|that) move fast\\b/i,\n /\\beverything you need to\\b|\\ball-in-one\\b/i,\n /\\beffortless(?:ly)?\\b|\\bin just a few clicks\\b/i,\n /\\bjoin (?:thousands|millions|hundreds) of\\b/i,\n /\\bchanges everything\\b/i,\n /\\bexperience the power of\\b/i,\n ].map(rule('marketing-phrase', 'Replace with the concrete outcome, number or workflow.')),\n rule('puffery', 'State the fact; let the reader judge it.')(/\\b(?:marks|represents) a pivotal moment\\b|\\bredefines the category\\b/i),\n rule('weasel-source', 'Name the source or the customer, or cut the claim.')(/\\bexperts agree\\b|\\bstudies show\\b|\\btrusted by (?:industry )?leaders\\b/i),\n rule('chummy-opener', 'Cut the chummy opener.')(/^(?:great question|let'?s be honest|we get it)\\b/im),\n rule('contrast-reveal', CONTRAST_FIX)(\n /\\bit'?s not (?:just )?[^.,;!?]{1,60}[,;.]\\s*it'?s\\b|\\b(?:this|that) isn'?t [^.;!?]{1,60}\\.\\s*(?:this|that|it) is\\b|\\bnot just [^.;!?]{1,60},? but\\b|\\bnot because [^.;!?]{1,60}\\.\\s*because\\b|\\bless [a-z]+, more [a-z]+\\b|\\bthe question isn'?t\\b|\\?\\s*think again\\b/i,\n ),\n rule('contrast-reveal', CONTRAST_FIX)(/(?:^|[.!?]\\s+)Not [^.;!?]{1,40}\\.\\s+[A-Z][^.;!?]{1,60}\\./),\n rule('negation-list', 'Say what does happen. One plain \"No card required\" beside the CTA is fine.')(\n /\\bno [a-z-]+(?: [a-z-]+)?[,.]\\s*no [a-z-]+/i,\n ),\n rule('question-reveal', 'Delete the question and keep the answer.')(\n /\\bthe (?:result|catch|best part|secret|kicker|twist|answer|difference|outcome|upshot)\\?\\s+\\S/i,\n ),\n rule('colon-reveal', 'Write it as a normal sentence.')(\n /\\b(?:the best part|here'?s the (?:secret|thing|kicker|catch)|one word|the result|the truth|the secret)\\s*:\\s/i,\n ),\n rule('hype-punctuation', 'Remove the stacked punctuation.')(/[!?]{2,}/),\n rule('decoration', 'Remove decorative unicode letters and emoji bullets.')(\n /[\\u{1D400}-\\u{1D7FF}]|^\\s*(?:[-*•]\\s*)?[\\u{1F300}-\\u{1FAFF}\\u{2600}-\\u{27BF}]/mu,\n ),\n]\n\nconst CHAT_WRAPPERS: readonly RegExp[] = [\n /^\\s*(?:sure|certainly|absolutely|of course|great)[!,.]/i,\n /^\\s*here(?:'s| is| are) (?:a|an|the|your|my) (?:draft|version|post|email|ad|headline|copy|subject|options?|revised)/i,\n /\\bi hope (?:this|that) helps\\b/i,\n /\\blet me know if you(?:'d like| want| need)\\b/i,\n /\\b(?:want|would you like) me to\\b/i,\n /\\bfeel free to (?:adjust|tweak|edit)\\b/i,\n]\n\n// An em dash anywhere; an en dash, double hyphen or single hyphen used as a\n// dash between words. A range like 10–20 has no spaces and is not a dash.\nconst DASH = /—|(?<=\\S)[ \\t]+(?:–|--?)[ \\t]+(?=\\S)/g\nconst CURLY = /[‘’“”]/\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** Inflections of one word: \"leverage\" → leverages, leveraged, leveraging. */\nfunction inflected(word: string): string {\n const stem = escapeRegExp(word)\n if (/e$/.test(word)) return `${stem.slice(0, -1)}(?:e|es|ed|ing|ely)`\n if (/[^aeiou]y$/.test(word)) return `${stem.slice(0, -1)}(?:y|ies|ied|ying|ily)`\n return `${stem}(?:s|es|ed|ing|ly)?`\n}\n\n/**\n * A whole-word matcher for a term. A single word (hyphenated or not) also\n * matches its inflections; a phrase matches as written. Hyphens and spaces in\n * a term match either, and a space matches any run of whitespace.\n */\nfunction termRegex(term: string): RegExp {\n const body = /\\s/.test(term.trim())\n ? escapeRegExp(term.trim())\n : inflected(term.trim())\n return new RegExp(`(?<![\\\\w-])${body.replace(/-|\\s+/g, '[-\\\\s]+')}(?![\\\\w-])`, 'gi')\n}\n\nconst BAN_REGEXES = BAN_WORDS.map((word) => ({ word, regex: termRegex(word) }))\nconst CAP_REGEXES = CAP_WORDS.map((word) => ({ word, regex: termRegex(word) }))\n\n/** The prose a reader reads: code, URLs and quote styles normalized away. */\nexport function proseOf(text: string): string {\n return text\n .replace(/```[\\s\\S]*?```/g, '\\n\\n')\n .replace(/`[^`\\n]*`/g, 'code')\n .replace(/https?:\\/\\/\\S+/g, 'link')\n .replace(/[‘’]/g, \"'\")\n .replace(/[“”]/g, '\"')\n}\n\nfunction paragraphsOf(prose: string): string[] {\n return prose.split(/\\n\\s*\\n/).map((p) => p.trim()).filter(Boolean)\n}\n\nfunction wordCount(text: string): number {\n return (text.match(/[A-Za-z0-9][\\w'-]*/g) ?? []).length\n}\n\nfunction repeatedTrigramRate(prose: string): number {\n const words = prose.toLowerCase().match(/[a-z0-9][a-z0-9'-]*/g) ?? []\n if (words.length < 12) return 0\n const seen = new Map<string, number>()\n for (let i = 0; i + 2 < words.length; i += 1) {\n const key = `${words[i]} ${words[i + 1]} ${words[i + 2]}`\n seen.set(key, (seen.get(key) ?? 0) + 1)\n }\n let repeats = 0\n for (const count of seen.values()) if (count > 1) repeats += count - 1\n return repeats / (words.length - 2)\n}\n\n/** Scan copy for AI tells. Deterministic and cheap; run it on every draft. */\nexport function scanCopy(text: string, options: CopyScanOptions = {}): CopyScanResult {\n const surface = options.surface ?? 'long'\n const prose = proseOf(text)\n const paragraphs = paragraphsOf(prose)\n // An extra already in the Ban tier (or listed twice) must not count twice.\n const extras = [...new Set((options.extraBanned ?? []).map((word) => word.toLowerCase()))]\n .filter((word) => !BAN_WORDS.includes(word))\n const banned = [...BAN_REGEXES, ...extras.map((word) => ({ word, regex: termRegex(word) }))]\n // A product that bans a cap-tier word outright gets one finding for it, not two.\n const capped = CAP_REGEXES.filter(({ word }) => !extras.includes(word))\n const findings: CopyTellFinding[] = []\n let contrastParagraphs = 0\n let tellWords = 0\n\n paragraphs.forEach((paragraph, index) => {\n for (const { word, regex } of banned) {\n for (const match of paragraph.matchAll(regex)) {\n tellWords += 1\n findings.push({ tier: 'ban', category: 'banned-word', match: match[0], paragraph: index, fix: `Replace \"${word}\" with the fact it stands in for.` })\n }\n }\n const capMatches = capped.flatMap(({ regex }) => [...paragraph.matchAll(regex)].map((match) => match[0]))\n tellWords += capMatches.length\n if (capMatches.length >= 2) {\n findings.push({ tier: 'cap', category: 'cap-words', match: capMatches.join(', '), paragraph: index, fix: 'Two cap-tier words in one paragraph: rewrite it from the facts.' })\n }\n let contrast = false\n for (const { pattern, category, fix } of PHRASE_RULES) {\n const match = paragraph.match(pattern)\n if (!match) continue\n if (category === 'contrast-reveal') {\n if (contrast) continue\n contrast = true\n }\n findings.push({ tier: 'ban', category, match: match[0].trim(), paragraph: index, fix })\n }\n if (contrast) contrastParagraphs += 1\n let run = 0\n for (const sentence of paragraph.split(/(?<=[.!?])\\s+/)) {\n const count = wordCount(sentence)\n run = count > 0 && count <= 3 ? run + 1 : 0\n if (run === 3) {\n findings.push({ tier: 'cap', category: 'fragment-drumbeat', match: sentence, paragraph: index, fix: 'Merge the fragments into one sentence that carries a fact.' })\n break\n }\n }\n })\n\n const dashes = prose.match(DASH)?.length ?? 0\n if (dashes > 0) {\n const tier: CopyTellTier = surface === 'short' ? 'ban' : dashes > 2 ? 'cap' : 'weak'\n const fix = surface === 'short'\n ? 'Short copy uses no dashes between words: use a period, comma or colon.'\n : 'Long copy allows two dashes per page at most; prefer a period or comma.'\n findings.push({ tier, category: 'dash', match: `${dashes} dash${dashes === 1 ? '' : 'es'}`, paragraph: -1, fix })\n }\n if (CURLY.test(text)) {\n findings.push({ tier: 'weak', category: 'decoration', match: 'curly quotes', paragraph: -1, fix: 'Weak alone: use the quote style the target format uses.' })\n }\n const wrapper = chatWrapperIn(text)\n if (wrapper !== null) {\n findings.push({ tier: 'ban', category: 'chat-wrapper', match: wrapper, paragraph: -1, fix: 'Return only the deliverable, with no chat around it.' })\n }\n\n const counts: Record<CopyTellTier, number> = { ban: 0, cap: 0, weak: 0 }\n for (const finding of findings) counts[finding.tier] += 1\n const words = wordCount(prose)\n // Saturates at 40 tell words per 1,000, a contrast in every paragraph, and 10% repeated trigrams.\n const wordRate = Math.min(1, (tellWords * 1000) / Math.max(1, words) / 40)\n const contrastRate = paragraphs.length === 0 ? 0 : contrastParagraphs / paragraphs.length\n const trigramRate = Math.min(1, repeatedTrigramRate(prose) * 10)\n const slopScore = Math.round(100 * (0.6 * wordRate + 0.25 * contrastRate + 0.15 * trigramRate))\n return { pass: counts.ban === 0 && counts.cap === 0, findings, counts, slopScore, words }\n}\n\n/** The chat wrapper around a deliverable (\"Here's a draft…\", \"Hope this helps\"), or null. */\nexport function chatWrapperIn(text: string): string | null {\n const prose = proseOf(text)\n for (const pattern of CHAT_WRAPPERS) {\n const match = prose.match(pattern)\n if (match) return match[0].trim()\n }\n return null\n}\n\n/**\n * Banned words and phrases for prompts that list them: base forms, lowercased,\n * deduplicated, product extras first. The scanner matches their inflections.\n */\nexport function bannedCopyVocabulary(extra: readonly string[] = []): string[] {\n return [...new Set([...extra, ...BAN_WORDS].map((word) => word.toLowerCase()))]\n}\n\nexport interface MetricClaim {\n /** The figure as written, e.g. \"$0.02/hr\", \"40%\", \"10x\", \"300ms\". */\n text: string\n /** Canonical value used to match it against sources (claim-support rules). */\n value: string\n}\n\n// A figure a reader takes as a measured fact: money, a percentage, a multiple,\n// a duration or size with its unit, a scaled count, or a grouped number.\n// Bare small integers (\"3 steps\") and years are not metrics.\nconst METRIC =\n /[$€£¥₹]\\s?\\d[\\d,]*(?:\\.\\d+)?(?:\\s?[kKmMbB]\\b)?|\\b\\d[\\d,]*(?:\\.\\d+)?\\s?(?:%|x\\b|×|ms\\b|s\\b|sec\\b|secs\\b|seconds?\\b|mins?\\b|minutes?\\b|hrs?\\b|hours?\\b|days?\\b|[kKmMbB]\\+?(?=\\s|$|[.,;])|[KMGT]B\\b|rps\\b|qps\\b|tokens?\\b)|\\b\\d{1,3}(?:,\\d{3})+(?:\\.\\d+)?\\b/g\n\n/** Every figure in `text` a reader would take as a measured fact. */\nexport function metricClaims(text: string): MetricClaim[] {\n const claims: MetricClaim[] = []\n for (const match of proseOf(text).matchAll(METRIC)) {\n const numeric = match[0].match(/[$€£¥₹]?\\s?\\d[\\d,]*(?:\\.\\d+)?/)?.[0]\n const value = numeric ? canonicalizeValue(numeric) : null\n if (value !== null) claims.push({ text: match[0].trim(), value })\n }\n return claims\n}\n\n/**\n * Metric claims in `copy` whose value appears in none of `sources`: figures\n * the writer cannot point to. Values match with claim-support's\n * canonicalization, so \"$1,200\" in copy matches \"1200.00\" in a source.\n */\nexport function unsourcedMetrics(copy: string, sources: readonly string[]): MetricClaim[] {\n const known = new Set(sources.flatMap((source) => valuesInText(source)))\n return metricClaims(copy).filter((claim) => !known.has(claim.value))\n}\n"],"mappings":"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","names":[]}
|