patina-cli 3.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.
- package/.patina.default.yaml +211 -0
- package/CHANGELOG.md +265 -0
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/README_JA.md +254 -0
- package/README_KR.md +253 -0
- package/README_ZH.md +254 -0
- package/SKILL-MAX.md +455 -0
- package/SKILL.md +730 -0
- package/assets/brand/patina-icon.svg +9 -0
- package/assets/brand/patina-logo.svg +17 -0
- package/assets/social/patina-before-after.svg +46 -0
- package/assets/social/patina-og.svg +31 -0
- package/bin/patina.js +9 -0
- package/core/scoring.md +657 -0
- package/core/standalone-prompt.md +364 -0
- package/core/stylometry.md +754 -0
- package/core/voice.md +163 -0
- package/docs/AUTHENTICATION.md +105 -0
- package/docs/AUTHENTICATION_KR.md +105 -0
- package/docs/BRANDING.md +37 -0
- package/docs/CLI.md +80 -0
- package/docs/COMPARISON.md +38 -0
- package/docs/COOKBOOK.md +173 -0
- package/docs/DEMO.md +40 -0
- package/docs/ETHICS.md +27 -0
- package/docs/EXAMPLES.md +130 -0
- package/docs/EXAMPLES_KR.md +130 -0
- package/docs/EXIT-CODES.md +25 -0
- package/docs/FAQ.md +67 -0
- package/docs/FAQ_KR.md +65 -0
- package/docs/FLAG-PARITY.md +53 -0
- package/docs/GLOSSARY.md +123 -0
- package/docs/PATTERNS-EN.md +718 -0
- package/docs/PATTERNS-JA.md +706 -0
- package/docs/PATTERNS-KO.md +707 -0
- package/docs/PATTERNS-ZH.md +706 -0
- package/docs/PATTERNS.md +22 -0
- package/docs/ROADMAP.md +315 -0
- package/docs/audits/2026-05-deep-research.md +290 -0
- package/docs/benchmarks/detector-comparison.json +442 -0
- package/docs/benchmarks/detector-comparison.md +65 -0
- package/docs/benchmarks/latest.json +988 -0
- package/docs/benchmarks/latest.md +112 -0
- package/docs/integrations/docker.md +19 -0
- package/docs/integrations/github-action.md +59 -0
- package/docs/integrations/pre-commit.md +77 -0
- package/docs/integrations/release.md +43 -0
- package/docs/internal/HARNESS.md +14 -0
- package/docs/internal/README.md +14 -0
- package/docs/internal/WARP.md +23 -0
- package/docs/research/2025-rebaseline-plan.md +89 -0
- package/docs/research/ai-human-metrics.md +380 -0
- package/docs/social/gstack-cardnews.html +236 -0
- package/docs/social/gstack-cardnews.md +88 -0
- package/docs/social/gstack-thread.md +106 -0
- package/docs/social/patina-launch-copy.md +227 -0
- package/docs/superpowers/specs/2026-04-03-meaning-preservation-design.md +299 -0
- package/lexicon/ai-en.md +162 -0
- package/lexicon/ai-ko.md +159 -0
- package/package.json +100 -0
- package/patina-max/SKILL.md +523 -0
- package/patina-max/composite.py +457 -0
- package/patterns/en-communication.md +89 -0
- package/patterns/en-content.md +133 -0
- package/patterns/en-filler.md +113 -0
- package/patterns/en-language.md +163 -0
- package/patterns/en-structure.md +173 -0
- package/patterns/en-style.md +139 -0
- package/patterns/en-viral-hook.md +211 -0
- package/patterns/ja-communication.md +101 -0
- package/patterns/ja-content.md +153 -0
- package/patterns/ja-filler.md +123 -0
- package/patterns/ja-language.md +190 -0
- package/patterns/ja-structure.md +142 -0
- package/patterns/ja-style.md +147 -0
- package/patterns/ja-viral-hook.md +216 -0
- package/patterns/ko-communication.md +98 -0
- package/patterns/ko-content.md +154 -0
- package/patterns/ko-filler.md +105 -0
- package/patterns/ko-language.md +182 -0
- package/patterns/ko-structure.md +147 -0
- package/patterns/ko-style.md +146 -0
- package/patterns/ko-viral-hook.md +211 -0
- package/patterns/zh-communication.md +101 -0
- package/patterns/zh-content.md +153 -0
- package/patterns/zh-filler.md +118 -0
- package/patterns/zh-language.md +173 -0
- package/patterns/zh-structure.md +145 -0
- package/patterns/zh-style.md +159 -0
- package/patterns/zh-viral-hook.md +216 -0
- package/profiles/academic.md +53 -0
- package/profiles/blog.md +81 -0
- package/profiles/casual-conversation.md +105 -0
- package/profiles/code-comment.md +104 -0
- package/profiles/commit-message.md +99 -0
- package/profiles/default.md +62 -0
- package/profiles/email.md +52 -0
- package/profiles/formal.md +98 -0
- package/profiles/instructional.md +80 -0
- package/profiles/legal.md +57 -0
- package/profiles/marketing.md +56 -0
- package/profiles/medical.md +53 -0
- package/profiles/narrative.md +79 -0
- package/profiles/release-notes.md +98 -0
- package/profiles/social.md +56 -0
- package/profiles/technical.md +53 -0
- package/scripts/benchmark-report.mjs +252 -0
- package/scripts/check-release-metadata.mjs +48 -0
- package/scripts/detector-comparison.mjs +267 -0
- package/scripts/lint.mjs +40 -0
- package/scripts/precommit-score.mjs +31 -0
- package/scripts/prose-score.mjs +186 -0
- package/scripts/update-benchmark-ranges.mjs +108 -0
- package/src/api.js +330 -0
- package/src/auth.js +105 -0
- package/src/backends/claude-cli.js +112 -0
- package/src/backends/codex-cli.js +121 -0
- package/src/backends/contract.js +21 -0
- package/src/backends/gemini-cli.js +135 -0
- package/src/backends/index.js +159 -0
- package/src/cache.js +106 -0
- package/src/cli.js +1280 -0
- package/src/commands/doctor.js +229 -0
- package/src/commands/init.js +208 -0
- package/src/config.js +126 -0
- package/src/errors.js +53 -0
- package/src/features/index.js +96 -0
- package/src/features/lexicon.js +90 -0
- package/src/features/segment.js +49 -0
- package/src/features/stylometry.js +50 -0
- package/src/loader.js +103 -0
- package/src/logger.js +70 -0
- package/src/manifest.js +162 -0
- package/src/max-mode.js +207 -0
- package/src/ouroboros.js +233 -0
- package/src/output.js +480 -0
- package/src/prompt-builder.js +409 -0
- package/src/providers.js +100 -0
- package/src/scoring.js +531 -0
- package/src/security.js +133 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-01.md +16 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-02.md +16 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-03.md +17 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-04.md +15 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-05.md +16 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-06-chat-register.md +16 -0
- package/tests/fixtures/suspect-zones/en/natural/en-nat-01.md +15 -0
- package/tests/fixtures/suspect-zones/en/natural/en-nat-02.md +15 -0
- package/tests/fixtures/suspect-zones/en/natural/en-nat-03.md +15 -0
- package/tests/fixtures/suspect-zones/en/natural/en-nat-04.md +15 -0
- package/tests/fixtures/suspect-zones/en/natural/en-nat-05.md +15 -0
- package/tests/fixtures/suspect-zones/expected-ranges.json +939 -0
- package/tests/fixtures/suspect-zones/ja/ai/ja-ai-01.md +11 -0
- package/tests/fixtures/suspect-zones/ja/ai/ja-ai-02.md +11 -0
- package/tests/fixtures/suspect-zones/ja/ai/ja-ai-03.md +11 -0
- package/tests/fixtures/suspect-zones/ja/natural/ja-nat-01.md +11 -0
- package/tests/fixtures/suspect-zones/ja/natural/ja-nat-02.md +11 -0
- package/tests/fixtures/suspect-zones/ja/natural/ja-nat-03.md +11 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-01.md +14 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-02.md +16 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-03.md +15 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-04.md +15 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-05.md +16 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-06-chat-register.md +16 -0
- package/tests/fixtures/suspect-zones/ko/natural/ko-nat-01.md +15 -0
- package/tests/fixtures/suspect-zones/ko/natural/ko-nat-02.md +15 -0
- package/tests/fixtures/suspect-zones/ko/natural/ko-nat-03.md +15 -0
- package/tests/fixtures/suspect-zones/ko/natural/ko-nat-04.md +14 -0
- package/tests/fixtures/suspect-zones/ko/natural/ko-nat-05.md +15 -0
- package/tests/fixtures/suspect-zones/zh/ai/zh-ai-01.md +11 -0
- package/tests/fixtures/suspect-zones/zh/ai/zh-ai-02.md +11 -0
- package/tests/fixtures/suspect-zones/zh/ai/zh-ai-03.md +11 -0
- package/tests/fixtures/suspect-zones/zh/natural/zh-nat-01.md +11 -0
- package/tests/fixtures/suspect-zones/zh/natural/zh-nat-02.md +11 -0
- package/tests/fixtures/suspect-zones/zh/natural/zh-nat-03.md +11 -0
- package/tests/quality/README.md +121 -0
- package/tests/quality/benchmark.mjs +306 -0
- package/tests/quality/detectors.manual.example.json +31 -0
- package/tests/quality/dogfood.mjs +44 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""patina-max composite: deterministic 4-axis winner reselection over an
|
|
3
|
+
existing patina-max run directory.
|
|
4
|
+
|
|
5
|
+
The default patina-max winner picker only sees AI-likeness and MPS, so it
|
|
6
|
+
goes noise-bound when a baseline is already humanized. This script adds two
|
|
7
|
+
Korean-aware deterministic metrics — Register Stability Score (RSS) and
|
|
8
|
+
Edit Conservativeness (EditCons) — and reselects the winner.
|
|
9
|
+
|
|
10
|
+
Usage
|
|
11
|
+
-----
|
|
12
|
+
python3 patina-max/composite.py <run_dir> [--weights ...]
|
|
13
|
+
|
|
14
|
+
Layout consumed
|
|
15
|
+
---------------
|
|
16
|
+
<run_dir>/
|
|
17
|
+
input.md baseline source MDX (required)
|
|
18
|
+
claude.md candidate (optional; absent → "missing")
|
|
19
|
+
gemini.md candidate (optional)
|
|
20
|
+
codex.md candidate (optional; may be a failure note)
|
|
21
|
+
meta.md YAML; per-candidate ai_score / mps / status (recommended)
|
|
22
|
+
|
|
23
|
+
Layout produced
|
|
24
|
+
---------------
|
|
25
|
+
<run_dir>/
|
|
26
|
+
composite.md per-candidate metric table + weighted totals
|
|
27
|
+
winner.md winning candidate's text (or a none-found notice)
|
|
28
|
+
|
|
29
|
+
Default weights (renormalised after dropping the LLM-Judge slot):
|
|
30
|
+
|
|
31
|
+
AI=0.353 MPS=0.235 RSS=0.235 EditCons=0.176
|
|
32
|
+
|
|
33
|
+
Override via .patina.default.yaml:
|
|
34
|
+
|
|
35
|
+
composite-weights:
|
|
36
|
+
ai: 0.353
|
|
37
|
+
mps: 0.235
|
|
38
|
+
rss: 0.235
|
|
39
|
+
edit_cons: 0.176
|
|
40
|
+
|
|
41
|
+
Or inline:
|
|
42
|
+
|
|
43
|
+
python3 patina-max/composite.py <run_dir> --weights ai=0.4,rss=0.3
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import argparse
|
|
49
|
+
import difflib
|
|
50
|
+
import math
|
|
51
|
+
import re
|
|
52
|
+
import sys
|
|
53
|
+
from collections import Counter
|
|
54
|
+
from dataclasses import dataclass, field
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from typing import Optional
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Korean register / edit metrics
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
# Sentence-final ending vocabulary. Order matters — longer forms first so the
|
|
64
|
+
# regex engine matches `합니다` before falling back to `다`.
|
|
65
|
+
_ENDING_PATTERNS = [
|
|
66
|
+
# 합쇼체 (deferential formal): ~ㅂ니다 / ~습니다 / ~ㅂ니까 / ~습니까 / ~십시오
|
|
67
|
+
("hapsho", r"(?:[가-힣]니다|[가-힣]니까|[가-힣]시오|십시오|십시요)"),
|
|
68
|
+
# 해요체 (polite informal)
|
|
69
|
+
("haeyo", r"(?:세요|예요|이에요|에요|해요|어요|아요|네요|군요|지요|죠|[가-힣]요)"),
|
|
70
|
+
# 해라체 (plain declarative / imperative)
|
|
71
|
+
("haera", r"(?:[가-힣]는다|한다|[가-힣]다|하라|마라|보라|들라|[가-힣]아라|[가-힣]어라|[가-힣]라)"),
|
|
72
|
+
# 해체 (casual / 반말)
|
|
73
|
+
("hae", r"(?:해|야|아|어|네|군|지)"),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
_SENTENCE_SPLIT = re.compile(r"[.!?。]+\s+|\n+")
|
|
77
|
+
_TRAILING_PUNCT = re.compile(r"[\s.,!?;:。、]+$")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _strip_markdown_noise(text: str) -> str:
|
|
81
|
+
"""Drop fenced code blocks, JSX tags, image lines, and href payloads.
|
|
82
|
+
|
|
83
|
+
Composite metrics are about Korean prose. MDX fences and JSX scaffolding
|
|
84
|
+
would otherwise inflate the token count and skew Edit Conservativeness.
|
|
85
|
+
"""
|
|
86
|
+
text = re.sub(r"```[\s\S]*?```", "", text)
|
|
87
|
+
text = re.sub(r"<[A-Z][\w]*\b[^>]*?/?>", "", text)
|
|
88
|
+
text = re.sub(r"</[A-Z][\w]*>", "", text)
|
|
89
|
+
text = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", text)
|
|
90
|
+
text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", text)
|
|
91
|
+
text = re.sub(r"\A---\n[\s\S]*?\n---\n", "", text)
|
|
92
|
+
return text
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _split_sentences(text: str) -> list[str]:
|
|
96
|
+
cleaned = _strip_markdown_noise(text)
|
|
97
|
+
parts = _SENTENCE_SPLIT.split(cleaned)
|
|
98
|
+
sentences: list[str] = []
|
|
99
|
+
for part in parts:
|
|
100
|
+
for line in part.splitlines():
|
|
101
|
+
line = line.strip()
|
|
102
|
+
if not line:
|
|
103
|
+
continue
|
|
104
|
+
line = re.sub(r"^\s*([>#\-*]+\s*)+", "", line)
|
|
105
|
+
line = re.sub(r"^\*\*[^*]+\*\*[\s:—-]*", "", line)
|
|
106
|
+
line = line.strip()
|
|
107
|
+
if line:
|
|
108
|
+
sentences.append(line)
|
|
109
|
+
return sentences
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def ending_distribution(text: str) -> Counter[str]:
|
|
113
|
+
dist: Counter[str] = Counter()
|
|
114
|
+
for sentence in _split_sentences(text):
|
|
115
|
+
tail = _TRAILING_PUNCT.sub("", sentence)
|
|
116
|
+
if not tail:
|
|
117
|
+
continue
|
|
118
|
+
bucket = "other"
|
|
119
|
+
for name, pattern in _ENDING_PATTERNS:
|
|
120
|
+
if re.search(pattern + r"$", tail):
|
|
121
|
+
bucket = name
|
|
122
|
+
break
|
|
123
|
+
dist[bucket] += 1
|
|
124
|
+
return dist
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def cosine_similarity(a: Counter[str], b: Counter[str]) -> float:
|
|
128
|
+
keys = set(a) | set(b)
|
|
129
|
+
if not keys:
|
|
130
|
+
return 0.0
|
|
131
|
+
dot = sum(a[k] * b[k] for k in keys)
|
|
132
|
+
norm_a = math.sqrt(sum(v * v for v in a.values()))
|
|
133
|
+
norm_b = math.sqrt(sum(v * v for v in b.values()))
|
|
134
|
+
if norm_a == 0 or norm_b == 0:
|
|
135
|
+
return 0.0
|
|
136
|
+
return dot / (norm_a * norm_b)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def register_stability(baseline: str, candidate: str) -> float:
|
|
140
|
+
"""RSS: cosine similarity of register distributions, scaled to 0-100."""
|
|
141
|
+
return cosine_similarity(ending_distribution(baseline), ending_distribution(candidate)) * 100.0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def edit_conservativeness(baseline: str, candidate: str) -> float:
|
|
145
|
+
"""EditCons: SequenceMatcher ratio on whitespace tokens (0-100)."""
|
|
146
|
+
base_tokens = _strip_markdown_noise(baseline).split()
|
|
147
|
+
cand_tokens = _strip_markdown_noise(candidate).split()
|
|
148
|
+
if not base_tokens and not cand_tokens:
|
|
149
|
+
return 100.0
|
|
150
|
+
if not base_tokens or not cand_tokens:
|
|
151
|
+
return 0.0
|
|
152
|
+
matcher = difflib.SequenceMatcher(None, base_tokens, cand_tokens, autojunk=False)
|
|
153
|
+
return matcher.ratio() * 100.0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Composite scoring + run-dir IO
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
DEFAULT_WEIGHTS = {
|
|
161
|
+
"ai": 0.353,
|
|
162
|
+
"mps": 0.235,
|
|
163
|
+
"rss": 0.235,
|
|
164
|
+
"edit_cons": 0.176,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
CANDIDATE_MODELS = ("claude", "gemini", "codex")
|
|
168
|
+
RUN_FRONTMATTER = re.compile(r"\A---\n([\s\S]*?)\n---\n", re.MULTILINE)
|
|
169
|
+
NUMBER_RANGE = re.compile(r"(\d+(?:\.\d+)?)\s*[-–~]\s*(\d+(?:\.\d+)?)")
|
|
170
|
+
SINGLE_NUMBER = re.compile(r"(\d+(?:\.\d+)?)")
|
|
171
|
+
NON_NUMERIC_PLACEHOLDERS = {"n/a", "na", "none", "—", "-", "pending", "tbd", "unknown"}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class Candidate:
|
|
176
|
+
model: str
|
|
177
|
+
text: str
|
|
178
|
+
ai_score: Optional[float] = None
|
|
179
|
+
mps: Optional[float] = None
|
|
180
|
+
rss: Optional[float] = None
|
|
181
|
+
edit_cons: Optional[float] = None
|
|
182
|
+
composite: Optional[float] = None
|
|
183
|
+
status: str = "unknown"
|
|
184
|
+
notes: list[str] = field(default_factory=list)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def parse_metric(raw: Optional[str]) -> Optional[float]:
|
|
188
|
+
"""Coerce metric strings from meta.md into floats.
|
|
189
|
+
|
|
190
|
+
`0-2 (within noise floor)` -> 1.0 (midpoint)
|
|
191
|
+
`92 (all anchors preserved)` -> 92.0
|
|
192
|
+
`n/a` / `pending` / `—` -> None
|
|
193
|
+
"""
|
|
194
|
+
if raw is None:
|
|
195
|
+
return None
|
|
196
|
+
raw = str(raw).strip().strip('"').strip("'")
|
|
197
|
+
if not raw or raw.lower() in NON_NUMERIC_PLACEHOLDERS:
|
|
198
|
+
return None
|
|
199
|
+
range_match = NUMBER_RANGE.search(raw)
|
|
200
|
+
if range_match:
|
|
201
|
+
return (float(range_match.group(1)) + float(range_match.group(2))) / 2.0
|
|
202
|
+
single_match = SINGLE_NUMBER.search(raw)
|
|
203
|
+
if single_match:
|
|
204
|
+
return float(single_match.group(1))
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def parse_meta_candidates(meta_text: str) -> dict[str, dict[str, str]]:
|
|
209
|
+
"""Pull per-candidate score lines from meta.md without a YAML library."""
|
|
210
|
+
info: dict[str, dict[str, str]] = {}
|
|
211
|
+
in_candidates = False
|
|
212
|
+
current: Optional[dict[str, str]] = None
|
|
213
|
+
for raw_line in meta_text.splitlines():
|
|
214
|
+
line = raw_line.rstrip()
|
|
215
|
+
if not line.startswith(" ") and line.endswith(":"):
|
|
216
|
+
in_candidates = line.strip() == "candidates:"
|
|
217
|
+
current = None
|
|
218
|
+
continue
|
|
219
|
+
if not in_candidates:
|
|
220
|
+
continue
|
|
221
|
+
stripped = line.lstrip()
|
|
222
|
+
if stripped.startswith("- model:"):
|
|
223
|
+
model = stripped.split(":", 1)[1].strip()
|
|
224
|
+
current = {"model": model}
|
|
225
|
+
info[model] = current
|
|
226
|
+
continue
|
|
227
|
+
if current is None:
|
|
228
|
+
continue
|
|
229
|
+
if ":" not in stripped:
|
|
230
|
+
continue
|
|
231
|
+
key, value = stripped.split(":", 1)
|
|
232
|
+
key = key.strip()
|
|
233
|
+
value = value.strip()
|
|
234
|
+
if value == "|":
|
|
235
|
+
value = "<multiline>"
|
|
236
|
+
if key in {"ai_score", "ai_score_instructional", "ai_score_technical", "mps", "status", "wall_time_seconds"}:
|
|
237
|
+
current[key] = value
|
|
238
|
+
return info
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def read_candidate_text(path: Path) -> str:
|
|
242
|
+
if not path.exists():
|
|
243
|
+
return ""
|
|
244
|
+
text = path.read_text(encoding="utf-8")
|
|
245
|
+
return RUN_FRONTMATTER.sub("", text, count=1)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def normalise_weights(weights: dict[str, float]) -> dict[str, float]:
|
|
249
|
+
total = sum(weights.values())
|
|
250
|
+
if total <= 0:
|
|
251
|
+
raise ValueError("weights must sum to a positive number")
|
|
252
|
+
return {k: v / total for k, v in weights.items()}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def parse_weight_overrides(spec: str) -> dict[str, float]:
|
|
256
|
+
overrides: dict[str, float] = {}
|
|
257
|
+
for chunk in spec.split(","):
|
|
258
|
+
chunk = chunk.strip()
|
|
259
|
+
if not chunk:
|
|
260
|
+
continue
|
|
261
|
+
if "=" not in chunk:
|
|
262
|
+
raise ValueError(f"invalid weight override `{chunk}` (expected key=value)")
|
|
263
|
+
key, value = chunk.split("=", 1)
|
|
264
|
+
key = key.strip().lower()
|
|
265
|
+
if key not in DEFAULT_WEIGHTS:
|
|
266
|
+
raise ValueError(f"unknown weight key `{key}`; valid: {sorted(DEFAULT_WEIGHTS)}")
|
|
267
|
+
try:
|
|
268
|
+
overrides[key] = float(value)
|
|
269
|
+
except ValueError as exc:
|
|
270
|
+
raise ValueError(f"weight `{key}` not a number: {value}") from exc
|
|
271
|
+
return overrides
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def load_yaml_weights(yaml_path: Path) -> dict[str, float]:
|
|
275
|
+
"""Pick `composite-weights:` out of the patina config without PyYAML."""
|
|
276
|
+
if not yaml_path.exists():
|
|
277
|
+
return {}
|
|
278
|
+
weights: dict[str, float] = {}
|
|
279
|
+
in_block = False
|
|
280
|
+
for raw_line in yaml_path.read_text(encoding="utf-8").splitlines():
|
|
281
|
+
if raw_line.startswith("composite-weights:"):
|
|
282
|
+
in_block = True
|
|
283
|
+
continue
|
|
284
|
+
if in_block:
|
|
285
|
+
if not raw_line.startswith(" "):
|
|
286
|
+
break
|
|
287
|
+
stripped = raw_line.strip()
|
|
288
|
+
if not stripped or stripped.startswith("#"):
|
|
289
|
+
continue
|
|
290
|
+
if ":" not in stripped:
|
|
291
|
+
break
|
|
292
|
+
key, value = stripped.split(":", 1)
|
|
293
|
+
key = key.strip().lower()
|
|
294
|
+
try:
|
|
295
|
+
weights[key] = float(value.strip().split("#", 1)[0].strip())
|
|
296
|
+
except ValueError:
|
|
297
|
+
continue
|
|
298
|
+
return {k: v for k, v in weights.items() if k in DEFAULT_WEIGHTS}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def resolve_weights(yaml_path: Path, cli_override: Optional[str]) -> dict[str, float]:
|
|
302
|
+
weights = dict(DEFAULT_WEIGHTS)
|
|
303
|
+
weights.update(load_yaml_weights(yaml_path))
|
|
304
|
+
if cli_override:
|
|
305
|
+
weights.update(parse_weight_overrides(cli_override))
|
|
306
|
+
return normalise_weights(weights)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def composite_score(candidate: Candidate, weights: dict[str, float]) -> Optional[float]:
|
|
310
|
+
if candidate.status != "success":
|
|
311
|
+
return None
|
|
312
|
+
if any(v is None for v in (candidate.ai_score, candidate.mps, candidate.rss, candidate.edit_cons)):
|
|
313
|
+
return None
|
|
314
|
+
return (
|
|
315
|
+
(100.0 - candidate.ai_score) * weights["ai"]
|
|
316
|
+
+ candidate.mps * weights["mps"]
|
|
317
|
+
+ candidate.rss * weights["rss"]
|
|
318
|
+
+ candidate.edit_cons * weights["edit_cons"]
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def render_composite_md(
|
|
323
|
+
run_dir: Path,
|
|
324
|
+
weights: dict[str, float],
|
|
325
|
+
candidates: list[Candidate],
|
|
326
|
+
winner: Optional[Candidate],
|
|
327
|
+
) -> str:
|
|
328
|
+
lines: list[str] = []
|
|
329
|
+
lines.append(f"# patina-composite scores for `{run_dir.name}`")
|
|
330
|
+
lines.append("")
|
|
331
|
+
lines.append("Generated by `patina-max/composite.py` — deterministic 4-axis reselection.")
|
|
332
|
+
lines.append("")
|
|
333
|
+
lines.append("## Weights")
|
|
334
|
+
lines.append("")
|
|
335
|
+
lines.append("| Axis | Weight |")
|
|
336
|
+
lines.append("|------|-------:|")
|
|
337
|
+
for key in ("ai", "mps", "rss", "edit_cons"):
|
|
338
|
+
lines.append(f"| {key} | {weights[key]:.4f} |")
|
|
339
|
+
lines.append("")
|
|
340
|
+
lines.append("## Candidate scores")
|
|
341
|
+
lines.append("")
|
|
342
|
+
lines.append("| Model | Status | AI | MPS | RSS | EditCons | Composite |")
|
|
343
|
+
lines.append("|-------|--------|---:|----:|----:|--------:|----------:|")
|
|
344
|
+
for cand in candidates:
|
|
345
|
+
ai = "—" if cand.ai_score is None else f"{cand.ai_score:.1f}"
|
|
346
|
+
mps = "—" if cand.mps is None else f"{cand.mps:.1f}"
|
|
347
|
+
rss = "—" if cand.rss is None else f"{cand.rss:.1f}"
|
|
348
|
+
edit = "—" if cand.edit_cons is None else f"{cand.edit_cons:.1f}"
|
|
349
|
+
comp = "—" if cand.composite is None else f"{cand.composite:.2f}"
|
|
350
|
+
lines.append(f"| {cand.model} | {cand.status} | {ai} | {mps} | {rss} | {edit} | {comp} |")
|
|
351
|
+
lines.append("")
|
|
352
|
+
if winner:
|
|
353
|
+
lines.append(f"**Winner:** `{winner.model}` — composite {winner.composite:.2f}")
|
|
354
|
+
else:
|
|
355
|
+
lines.append("**Winner:** none (no candidate scored successfully)")
|
|
356
|
+
lines.append("")
|
|
357
|
+
if any(cand.notes for cand in candidates):
|
|
358
|
+
lines.append("")
|
|
359
|
+
lines.append("## Notes")
|
|
360
|
+
for cand in candidates:
|
|
361
|
+
for note in cand.notes:
|
|
362
|
+
lines.append(f"- **{cand.model}**: {note}")
|
|
363
|
+
return "\n".join(lines) + "\n"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
367
|
+
parser = argparse.ArgumentParser(description="patina-max composite winner reselection.")
|
|
368
|
+
parser.add_argument("run_dir", type=Path, help="path to a patina-max run dir")
|
|
369
|
+
parser.add_argument(
|
|
370
|
+
"--weights",
|
|
371
|
+
type=str,
|
|
372
|
+
default=None,
|
|
373
|
+
help="override weights, comma-separated (e.g. ai=0.4,rss=0.3)",
|
|
374
|
+
)
|
|
375
|
+
parser.add_argument(
|
|
376
|
+
"--config",
|
|
377
|
+
type=Path,
|
|
378
|
+
default=Path(__file__).resolve().parents[1] / ".patina.default.yaml",
|
|
379
|
+
help="patina config to read composite-weights from",
|
|
380
|
+
)
|
|
381
|
+
args = parser.parse_args(argv)
|
|
382
|
+
|
|
383
|
+
run_dir: Path = args.run_dir.resolve()
|
|
384
|
+
if not run_dir.is_dir():
|
|
385
|
+
print(f"error: not a directory: {run_dir}", file=sys.stderr)
|
|
386
|
+
return 2
|
|
387
|
+
|
|
388
|
+
input_path = run_dir / "input.md"
|
|
389
|
+
if not input_path.exists():
|
|
390
|
+
print(f"error: missing required file: {input_path}", file=sys.stderr)
|
|
391
|
+
return 2
|
|
392
|
+
baseline = read_candidate_text(input_path)
|
|
393
|
+
|
|
394
|
+
meta_path = run_dir / "meta.md"
|
|
395
|
+
meta_info: dict[str, dict[str, str]] = {}
|
|
396
|
+
if meta_path.exists():
|
|
397
|
+
meta_info = parse_meta_candidates(meta_path.read_text(encoding="utf-8"))
|
|
398
|
+
else:
|
|
399
|
+
print(f"warning: missing meta.md at {meta_path}; AI/MPS will be marked unknown", file=sys.stderr)
|
|
400
|
+
|
|
401
|
+
weights = resolve_weights(args.config, args.weights)
|
|
402
|
+
|
|
403
|
+
candidates: list[Candidate] = []
|
|
404
|
+
for model in CANDIDATE_MODELS:
|
|
405
|
+
path = run_dir / f"{model}.md"
|
|
406
|
+
if not path.exists():
|
|
407
|
+
candidates.append(Candidate(model=model, text="", status="missing"))
|
|
408
|
+
continue
|
|
409
|
+
text = read_candidate_text(path)
|
|
410
|
+
info = meta_info.get(model, {})
|
|
411
|
+
cand = Candidate(
|
|
412
|
+
model=model,
|
|
413
|
+
text=text,
|
|
414
|
+
status=info.get("status", "unknown"),
|
|
415
|
+
ai_score=parse_metric(info.get("ai_score") or info.get("ai_score_instructional") or info.get("ai_score_technical")),
|
|
416
|
+
mps=parse_metric(info.get("mps")),
|
|
417
|
+
)
|
|
418
|
+
if cand.status == "success" and cand.text.strip():
|
|
419
|
+
cand.rss = register_stability(baseline, cand.text)
|
|
420
|
+
cand.edit_cons = edit_conservativeness(baseline, cand.text)
|
|
421
|
+
else:
|
|
422
|
+
cand.notes.append("skipping deterministic metrics (status not success or empty text)")
|
|
423
|
+
cand.composite = composite_score(cand, weights)
|
|
424
|
+
if cand.status == "success" and cand.composite is None:
|
|
425
|
+
cand.notes.append("composite undefined — at least one of AI/MPS could not be parsed from meta.md")
|
|
426
|
+
candidates.append(cand)
|
|
427
|
+
|
|
428
|
+
scored = [c for c in candidates if c.composite is not None]
|
|
429
|
+
winner: Optional[Candidate] = max(scored, key=lambda c: c.composite) if scored else None
|
|
430
|
+
|
|
431
|
+
composite_path = run_dir / "composite.md"
|
|
432
|
+
composite_path.write_text(
|
|
433
|
+
render_composite_md(run_dir, weights, candidates, winner),
|
|
434
|
+
encoding="utf-8",
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
winner_path = run_dir / "winner.md"
|
|
438
|
+
if winner is not None:
|
|
439
|
+
winner_path.write_text(
|
|
440
|
+
f"---\nwinner_model: {winner.model}\ncomposite_score: {winner.composite:.2f}\n---\n\n{winner.text.lstrip()}",
|
|
441
|
+
encoding="utf-8",
|
|
442
|
+
)
|
|
443
|
+
else:
|
|
444
|
+
winner_path.write_text("# winner.md\n\nNo candidate scored successfully.\n", encoding="utf-8")
|
|
445
|
+
|
|
446
|
+
cwd = Path.cwd()
|
|
447
|
+
print(f"wrote {composite_path.relative_to(cwd) if composite_path.is_relative_to(cwd) else composite_path}")
|
|
448
|
+
print(f"wrote {winner_path.relative_to(cwd) if winner_path.is_relative_to(cwd) else winner_path}")
|
|
449
|
+
if winner:
|
|
450
|
+
print(f"winner: {winner.model} (composite {winner.composite:.2f})")
|
|
451
|
+
else:
|
|
452
|
+
print("winner: none")
|
|
453
|
+
return 0
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
if __name__ == "__main__":
|
|
457
|
+
sys.exit(main())
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
pack: en-communication
|
|
3
|
+
language: en
|
|
4
|
+
name: Communication Patterns
|
|
5
|
+
version: 1.1.0
|
|
6
|
+
patterns: 4
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Communication Patterns
|
|
10
|
+
|
|
11
|
+
### 19. Collaborative Communication Artifacts
|
|
12
|
+
|
|
13
|
+
**Watch words:** I hope this helps!, Let me know if you need, Feel free to ask, Happy to help, Don't hesitate to reach out, I'd be glad to, Is there anything else, Hope that clarifies things, Let me know if you'd like me to
|
|
14
|
+
|
|
15
|
+
**Fire condition:** Any chatbot-style conversational phrase appears in content that is not a live interactive conversation.
|
|
16
|
+
|
|
17
|
+
**Exclusion:** Acceptable in transcripts of actual live chat sessions, intentional UI microcopy for chatbot interfaces, or dialogue being quoted and analyzed.
|
|
18
|
+
|
|
19
|
+
**Problem:** AI includes chatbot conversational phrases in written content. These are appropriate in a live chat but not in articles, reports, or documentation.
|
|
20
|
+
|
|
21
|
+
**Semantic Risk:** LOW
|
|
22
|
+
|
|
23
|
+
**Before:**
|
|
24
|
+
> The French Revolution began in 1789, driven by fiscal crisis and food shortages. I hope this helps! Let me know if you need more details on any specific aspect of the revolution. Feel free to ask about the key figures involved.
|
|
25
|
+
|
|
26
|
+
**After:**
|
|
27
|
+
> The French Revolution began in 1789, driven by fiscal crisis and food shortages. The immediate trigger was the near-bankruptcy of the French state and a bread price spike that hit Paris hardest.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
### 20. Knowledge-Cutoff Disclaimers
|
|
32
|
+
|
|
33
|
+
**Watch words:** as of my last update, I don't have access to real-time, my training data, I cannot verify current, as of my knowledge cutoff, I'm not able to browse, please verify this information, this may have changed since
|
|
34
|
+
|
|
35
|
+
**Fire condition:** Any AI self-reference or training-data caveat appears in editorial, journalistic, or analytical content.
|
|
36
|
+
|
|
37
|
+
**Exclusion:** Acceptable in technical documentation explicitly about AI systems, or in content that intentionally discloses AI generation (with a proper disclosure note). Also acceptable if the caveat is replaced with a dated, source-cited fact.
|
|
38
|
+
|
|
39
|
+
**Problem:** AI includes training-data caveats in content that should not reference AI limitations. These disclaimers break the fourth wall and remind readers the text was machine-generated.
|
|
40
|
+
|
|
41
|
+
**Semantic Risk:** MEDIUM
|
|
42
|
+
**Preservation Note:** The disclaimer sometimes carries a genuine factual caveat about data currency; removing it without replacing the claim with a dated source may leave an unverified assertion that the reader cannot assess.
|
|
43
|
+
|
|
44
|
+
**Before:**
|
|
45
|
+
> As of my last update in April 2024, the company had around 5,000 employees. I don't have access to real-time data, so please verify this information with current sources.
|
|
46
|
+
|
|
47
|
+
**After:**
|
|
48
|
+
> The company had about 5,000 employees as of its 2024 annual report.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### 21. Sycophantic/Servile Tone
|
|
53
|
+
|
|
54
|
+
**Watch words:** Great question!, That's an excellent point, You've raised a fascinating, What a thoughtful, That's a really interesting, Absolutely!, You're absolutely right, What a great observation
|
|
55
|
+
|
|
56
|
+
**Fire condition:** Any flattering or servile opener appears before substantive content.
|
|
57
|
+
|
|
58
|
+
**Exclusion:** None — this pattern has no valid context in editorial, analytical, or journalistic writing. Even in conversational formats, drop the flattery and start with the answer.
|
|
59
|
+
|
|
60
|
+
**Problem:** AI flatters the reader or questioner before answering. This servile opener adds no information and signals AI generation.
|
|
61
|
+
|
|
62
|
+
**Semantic Risk:** LOW
|
|
63
|
+
|
|
64
|
+
**Before:**
|
|
65
|
+
> Great question! That's a really fascinating topic. You've raised an excellent point about the economic factors at play. Let me break this down for you.
|
|
66
|
+
|
|
67
|
+
**After:**
|
|
68
|
+
> The main economic factor is the gap between housing supply and demand. Building permits in the metro area dropped 18% last year while population grew 2.1%.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### 29. False Nuance (Retroactive Reframing)
|
|
73
|
+
|
|
74
|
+
**Watch words:** Actually, it's more nuanced than that, To be more precise, Well, it's not quite that simple, That said, the reality is more complex, More accurately, In fairness, it's more that, If we're being precise, Though to be fair
|
|
75
|
+
|
|
76
|
+
**Fire condition:** The text restates or lightly reframes the preceding claim under the guise of adding nuance, without introducing new information, evidence, or a genuinely different perspective.
|
|
77
|
+
|
|
78
|
+
**Exclusion:** Acceptable when the reframe introduces a substantive correction, cites new evidence, or genuinely shifts the analytical frame in a way that changes the conclusion. Also acceptable in dialogue transcripts where the speaker is visibly self-correcting with new data.
|
|
79
|
+
|
|
80
|
+
**Problem:** AI hedges its own statements by immediately re-qualifying them in a way that sounds thoughtful but adds nothing. The "nuance" is cosmetic — the second sentence says the same thing as the first in slightly different words. This creates an illusion of depth while padding the text.
|
|
81
|
+
|
|
82
|
+
**Semantic Risk:** HIGH
|
|
83
|
+
**Preservation Note:** Removing the reframing clause may delete a genuine qualification or exception; verify that the retained primary claim accurately stands alone before removing the "nuance" sentence.
|
|
84
|
+
|
|
85
|
+
**Before:**
|
|
86
|
+
> Remote work increases productivity. Actually, it's more nuanced than that — remote work can enhance productivity in certain contexts while presenting challenges in others, and the net effect depends on organizational culture and individual work styles.
|
|
87
|
+
|
|
88
|
+
**After:**
|
|
89
|
+
> Remote work increases productivity for focused solo tasks — a Stanford study found a 13% gain for call center workers. It hurts spontaneous collaboration, though: Microsoft's 2021 internal data showed cross-team communication dropped 25% after going fully remote.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
pack: en-content
|
|
3
|
+
language: en
|
|
4
|
+
name: Content Patterns
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
patterns: 6
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Content Patterns
|
|
10
|
+
|
|
11
|
+
### 1. Undue Emphasis on Significance
|
|
12
|
+
|
|
13
|
+
**Watch words:** significant milestone, pivotal moment, groundbreaking, transformative, paradigm shift, revolutionary, game-changing, unprecedented, landmark achievement, watershed moment, trailblazing, monumental
|
|
14
|
+
|
|
15
|
+
**Fire condition:** 2+ emphasis words appear in the same paragraph, or a single word like "revolutionary" or "groundbreaking" applied to an ordinary product or event.
|
|
16
|
+
|
|
17
|
+
**Exclusion:** Genuine historical events of large scale (first moon landing, eradication of a disease) where the adjective is proportionate. Use judgment on actual impact.
|
|
18
|
+
|
|
19
|
+
**Semantic Risk:** HIGH
|
|
20
|
+
**Preservation Note:** Removing emphasis words may delete the author's actual claim about significance; over-correction can convert a genuinely notable event into an unremarkable one.
|
|
21
|
+
|
|
22
|
+
**Problem:** AI inflates the importance of ordinary topics. Everything becomes a "significant milestone" or a "paradigm shift," regardless of actual impact.
|
|
23
|
+
|
|
24
|
+
**Before:**
|
|
25
|
+
> The company's new mobile app represents a groundbreaking paradigm shift in how users interact with grocery delivery services. This transformative, game-changing platform marks an unprecedented milestone in the retail industry.
|
|
26
|
+
|
|
27
|
+
**After:**
|
|
28
|
+
> The company launched a grocery delivery app. It lets users schedule same-day deliveries and track orders in real time. Downloads hit 2 million in the first month.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### 2. Undue Emphasis on Notability/Media
|
|
33
|
+
|
|
34
|
+
**Watch words:** garnered significant attention, widely recognized, has been featured in, attracted widespread interest, gained international acclaim, made headlines, captured the imagination of, has been praised by critics and audiences alike
|
|
35
|
+
|
|
36
|
+
**Fire condition:** A claim of broad attention, coverage, or acclaim appears without a named publication, outlet, or specific figure.
|
|
37
|
+
|
|
38
|
+
**Exclusion:** Statements like "widely used" backed by a rough scale ("used in 50 countries", "over 10 million installs") — specificity makes them acceptable even without named sources.
|
|
39
|
+
|
|
40
|
+
**Semantic Risk:** HIGH
|
|
41
|
+
**Preservation Note:** Correcting unsourced attention claims may remove a core assertion about recognition or impact that the author intended as a factual claim.
|
|
42
|
+
|
|
43
|
+
**Problem:** AI claims broad media coverage or public attention without citing specific sources or evidence.
|
|
44
|
+
|
|
45
|
+
**Before:**
|
|
46
|
+
> Her artwork has garnered significant attention from critics and audiences alike, and has been widely recognized as a defining voice of her generation. Her exhibitions have attracted widespread interest across the globe.
|
|
47
|
+
|
|
48
|
+
**After:**
|
|
49
|
+
> The New York Times reviewed her 2023 exhibition, calling her use of recycled materials "quietly radical." The show sold out its three-week run at the Whitechapel Gallery.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### 3. Superficial -ing Analyses
|
|
54
|
+
|
|
55
|
+
**Watch words:** showcasing, highlighting, underscoring, demonstrating, illustrating, reflecting, signaling, exemplifying, reinforcing, embodying, encapsulating
|
|
56
|
+
|
|
57
|
+
**Fire condition:** 3+ present-participle phrases chained in a single sentence or consecutive clauses with no concrete causal explanation.
|
|
58
|
+
|
|
59
|
+
**Exclusion:** A single well-placed participle for genuine causal or temporal connection ("the policy increased costs, pushing firms to cut staff") is acceptable and not this pattern.
|
|
60
|
+
|
|
61
|
+
**Semantic Risk:** HIGH
|
|
62
|
+
**Preservation Note:** Replacing participle chains with concrete explanation restructures the argument; if the causal relationship is not well understood, the correction may introduce a claim the original did not make.
|
|
63
|
+
|
|
64
|
+
**Problem:** AI uses present participle chains as filler analysis. Instead of explaining *why* something matters, it strings together "-ing" words that gesture at significance without saying anything concrete.
|
|
65
|
+
|
|
66
|
+
**Before:**
|
|
67
|
+
> The festival brings together artists from 30 countries, showcasing the diversity of contemporary dance, highlighting the importance of cross-cultural dialogue, and underscoring the role of the arts in fostering global understanding.
|
|
68
|
+
|
|
69
|
+
**After:**
|
|
70
|
+
> The festival brings together artists from 30 countries. This year, a butoh troupe from Tokyo collaborated with a hip-hop crew from Lagos — a pairing that would not have happened without the festival's residency program.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### 4. Promotional Language
|
|
75
|
+
|
|
76
|
+
**Watch words:** stunning, breathtaking, world-class, gem of, hidden treasure, crown jewel, vibrant, nestled in, boasts, a must-visit, unparalleled, exquisite, awe-inspiring, picturesque
|
|
77
|
+
|
|
78
|
+
**Fire condition:** 2+ promotional adjectives modifying the same subject, or a single strong superlative ("world-class", "breathtaking", "must-visit") used as descriptive prose rather than quoted marketing copy.
|
|
79
|
+
|
|
80
|
+
**Exclusion:** Direct quotations from marketing or promotional materials being analyzed — the promotional language belongs to the source, not the author.
|
|
81
|
+
|
|
82
|
+
**Semantic Risk:** MEDIUM
|
|
83
|
+
**Preservation Note:** Replacing promotional adjectives with neutral description may remove genuine qualitative assessments the author intended as their own voice or evaluation.
|
|
84
|
+
|
|
85
|
+
**Problem:** AI uses tourism-brochure language instead of neutral description, especially when writing about places, food, or cultural events.
|
|
86
|
+
|
|
87
|
+
**Before:**
|
|
88
|
+
> Nestled in the rolling hills of Tuscany, this stunning village boasts breathtaking views, world-class cuisine, and an exquisite charm that makes it a must-visit hidden gem for any discerning traveler.
|
|
89
|
+
|
|
90
|
+
**After:**
|
|
91
|
+
> The village sits on a hill about 40 minutes south of Florence. It has one restaurant, a weekly market on Thursdays, and a 14th-century church with frescoes that are slowly being restored.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### 5. Vague Attributions
|
|
96
|
+
|
|
97
|
+
**Watch words:** experts say, many believe, it is widely accepted, studies show, research indicates, critics argue, according to sources, observers note, analysts predict, some suggest, it is generally agreed
|
|
98
|
+
|
|
99
|
+
**Fire condition:** Any claim of authority appears with an unspecified source rather than a named one (person, institution, publication, or study with date).
|
|
100
|
+
|
|
101
|
+
**Exclusion:** Well-established consensus facts with no reasonable controversy ("doctors say smoking causes lung cancer") — use judgment on whether a named source is genuinely necessary.
|
|
102
|
+
|
|
103
|
+
**Semantic Risk:** HIGH
|
|
104
|
+
**Preservation Note:** Removing vague attributions without replacement deletes the author's sourcing claim; correction must substitute a real source or remove the claim entirely, which changes the text's evidentiary basis.
|
|
105
|
+
|
|
106
|
+
**Problem:** AI cites unnamed "experts" and "studies" instead of specific sources. This creates an illusion of authority without any verifiable backing.
|
|
107
|
+
|
|
108
|
+
**Before:**
|
|
109
|
+
> Experts say that remote work is here to stay. Studies show that productivity increases when employees work from home, and many believe this trend will reshape the commercial real estate market.
|
|
110
|
+
|
|
111
|
+
**After:**
|
|
112
|
+
> A 2023 Stanford study by Nicholas Bloom found that hybrid workers were 3% more productive than full-time office workers. Kastle Systems data shows U.S. office occupancy has stabilized at about 50% of pre-pandemic levels.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### 6. Formulaic "Challenges and Prospects"
|
|
117
|
+
|
|
118
|
+
**Watch words:** despite these challenges, remains to be seen, poised for growth, at a crossroads, on the cusp of, only time will tell, the road ahead, faces significant hurdles but, with continued effort, looking forward
|
|
119
|
+
|
|
120
|
+
**Fire condition:** A paragraph or conclusion contains both a generic challenge phrase AND a generic optimism phrase — the classic two-step pattern of acknowledging problems then pivoting to vague hope.
|
|
121
|
+
|
|
122
|
+
**Exclusion:** Genuine uncertainty expressed with specific caveats ("FDA approval takes 14 months; if denied by Q3, we shift to EU trials") — precision makes it acceptable. Only trigger when both poles are vague.
|
|
123
|
+
|
|
124
|
+
**Semantic Risk:** HIGH
|
|
125
|
+
**Preservation Note:** Replacing the challenge-prospect formula restructures the argument's conclusion; the corrected version must not omit actual challenges or prospects the author named, even vaguely.
|
|
126
|
+
|
|
127
|
+
**Problem:** AI wraps up with a generic challenges-then-optimism formula: acknowledge problems, then pivot to vague hope. This pattern appears at the end of almost every AI-generated article or essay.
|
|
128
|
+
|
|
129
|
+
**Before:**
|
|
130
|
+
> Despite these challenges, the industry remains poised for significant growth. While it remains to be seen how regulations will evolve, the sector stands at a crossroads, and with continued innovation and collaboration, a bright future lies ahead.
|
|
131
|
+
|
|
132
|
+
**After:**
|
|
133
|
+
> The biggest obstacle is the FDA approval timeline — the average wait is 14 months. Two of the five pending applications were filed before 2022 and still have no decision date. The company says it will shift trials to the EU if U.S. approval is not granted by Q3.
|