patina-cli 3.11.0 → 4.0.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 +29 -29
- package/CHANGELOG.md +53 -0
- package/NOTICE +21 -0
- package/README.md +117 -224
- package/README_JA.md +134 -77
- package/README_KR.md +132 -74
- package/README_ZH.md +137 -80
- package/SKILL.md +11 -20
- package/artifacts/rebaseline-2025/README.md +147 -0
- package/artifacts/rebaseline-2025/human-controls.public.jsonl +250 -0
- package/artifacts/rebaseline-2025/intake.example.jsonl +2 -0
- package/artifacts/rebaseline-2025/intake.local.example.jsonl +25 -0
- package/artifacts/rebaseline-2025/prompts.template.jsonl +7 -0
- package/artifacts/rebaseline-2025/sources.ko-public.jsonl +39 -0
- package/assets/brand/patina-badge.svg +18 -0
- package/assets/brand/patina-mark.svg +8 -0
- package/assets/demo/README.md +79 -0
- package/core/scoring.md +12 -12
- package/core/standalone-prompt.md +3 -1
- package/core/stylometry.md +93 -22
- package/docs/API.md +1554 -0
- package/docs/AUTHENTICATION.md +50 -26
- package/docs/AUTHENTICATION_KR.md +54 -29
- package/docs/BRANDING.md +9 -8
- package/docs/CLI.md +55 -14
- package/docs/COOKBOOK.md +8 -21
- package/docs/DEMO.md +32 -5
- package/docs/EXIT-CODES.md +2 -3
- package/docs/FALSE-POSITIVES.md +63 -0
- package/docs/FAQ.md +9 -1
- package/docs/FAQ_KR.md +3 -1
- package/docs/FLAG-PARITY.md +33 -47
- package/docs/ISSUE-WAVES.md +57 -0
- package/docs/PATTERNS-EN.md +67 -3
- package/docs/PATTERNS-JA.md +68 -2
- package/docs/PATTERNS-KO.md +70 -7
- package/docs/PATTERNS-ZH.md +67 -3
- package/docs/PATTERNS.md +5 -5
- package/docs/RESEARCH-DOCS-PLATFORM.md +54 -0
- package/docs/ROADMAP.md +46 -66
- package/docs/TRANSLATIONESE-KO.md +51 -0
- package/docs/audits/2026-05-deep-research.md +3 -1
- package/docs/benchmarks/README.md +51 -0
- package/docs/benchmarks/detector-comparison.json +69 -9
- package/docs/benchmarks/detector-comparison.md +10 -5
- package/docs/benchmarks/katfish-ko-latest.json +657 -0
- package/docs/benchmarks/katfish-ko-latest.md +77 -0
- package/docs/benchmarks/latest.json +1183 -108
- package/docs/benchmarks/latest.md +84 -60
- package/docs/benchmarks/lexicon-freshness-en-2026-05-22.json +1121 -0
- package/docs/benchmarks/lexicon-freshness-en-2026-05-22.md +136 -0
- package/docs/benchmarks/rebaseline-latest.json +381 -0
- package/docs/benchmarks/rebaseline-latest.md +121 -0
- package/docs/benchmarks/register-stratified-latest.json +164 -0
- package/docs/benchmarks/register-stratified-latest.md +99 -0
- package/docs/benchmarks/register-stratified.md +43 -0
- package/docs/integrations/github-action.md +44 -11
- package/docs/integrations/playground.md +58 -0
- package/docs/integrations/pre-commit.md +5 -5
- package/docs/integrations/release.md +5 -3
- package/docs/integrations/static-sites.md +83 -0
- package/docs/research/2025-rebaseline-plan.md +71 -2
- package/docs/research/2026-rebaseline.md +102 -0
- package/docs/research/adversarial-mps.md +41 -0
- package/docs/research/ai-human-metrics.md +35 -23
- package/docs/research/human-eval-panel.md +42 -0
- package/docs/research/judge-agreement.md +24 -0
- package/docs/research/ko-2025-corpus-sources.md +135 -0
- package/docs/research/lexicon-freshness-audit.md +64 -0
- package/docs/research/zh-ja-lexicon-calibration.md +60 -0
- package/docs/social/patina-launch-copy.md +173 -100
- package/docs/social/patina-launch-execution.md +94 -0
- package/docs/social/patina-launch-korean-first.md +83 -0
- package/docs/social/signs-of-ai-writing.md +26 -0
- package/docs/social/signs-of-ai-writing_KR.md +26 -0
- package/lexicon/ai-en.md +21 -24
- package/lexicon/ai-ja.md +158 -0
- package/lexicon/ai-ko.md +9 -9
- package/lexicon/ai-zh.md +158 -0
- package/lexicon/provenance/ai-en.json +970 -0
- package/lexicon/provenance/ai-ja.json +542 -0
- package/lexicon/provenance/ai-ko.json +866 -0
- package/lexicon/provenance/ai-zh.json +542 -0
- package/package.json +49 -8
- package/patterns/en-communication.md +5 -0
- package/patterns/en-content.md +5 -0
- package/patterns/en-filler.md +5 -0
- package/patterns/en-language.md +29 -1
- package/patterns/en-structure.md +5 -0
- package/patterns/en-style.md +5 -0
- package/patterns/en-viral-hook.md +42 -2
- package/patterns/ja-communication.md +5 -0
- package/patterns/ja-content.md +5 -0
- package/patterns/ja-filler.md +5 -0
- package/patterns/ja-language.md +33 -1
- package/patterns/ja-structure.md +12 -0
- package/patterns/ja-style.md +5 -0
- package/patterns/ja-viral-hook.md +41 -2
- package/patterns/ko-communication.md +5 -0
- package/patterns/ko-content.md +5 -0
- package/patterns/ko-filler.md +5 -0
- package/patterns/ko-language.md +33 -1
- package/patterns/ko-structure.md +25 -6
- package/patterns/ko-style.md +5 -0
- package/patterns/ko-viral-hook.md +38 -2
- package/patterns/zh-communication.md +5 -0
- package/patterns/zh-content.md +5 -0
- package/patterns/zh-filler.md +5 -0
- package/patterns/zh-language.md +37 -1
- package/patterns/zh-structure.md +12 -0
- package/patterns/zh-style.md +5 -0
- package/patterns/zh-viral-hook.md +38 -2
- package/playground/README.md +55 -0
- package/playground/analytics.js +4 -0
- package/playground/analyzer.js +883 -0
- package/playground/app.js +157 -0
- package/playground/data/lexicons.js +343 -0
- package/playground/index.html +138 -0
- package/playground/styles.css +267 -0
- package/profiles/namuwiki.md +111 -0
- package/scripts/adversarial-mps-report.mjs +201 -0
- package/scripts/badge-json.mjs +79 -0
- package/scripts/benchmark-report.mjs +56 -9
- package/scripts/check-release-metadata.mjs +0 -2
- package/scripts/detector-comparison.mjs +7 -7
- package/scripts/generate-playground-data.mjs +77 -0
- package/scripts/katfish-calibration.mjs +464 -0
- package/scripts/lexicon-freshness.mjs +485 -0
- package/scripts/lint.mjs +1 -1
- package/scripts/precommit-score.mjs +4 -3
- package/scripts/prose-score.mjs +81 -5
- package/scripts/rebaseline-intake.mjs +242 -0
- package/scripts/rebaseline-score.mjs +268 -0
- package/scripts/rebaseline-summary.mjs +773 -0
- package/scripts/rebaseline-web-collect.mjs +410 -0
- package/scripts/update-benchmark-ranges.mjs +1 -0
- package/src/api.js +69 -105
- package/src/auth.js +50 -2
- package/src/backends/claude-cli.js +19 -4
- package/src/backends/codex-cli.js +19 -3
- package/src/backends/contract.js +230 -1
- package/src/backends/gemini-cli.js +18 -5
- package/src/backends/index.js +87 -12
- package/src/backends/kimi-cli.js +161 -0
- package/src/cli.js +577 -567
- package/src/commands/doctor.js +2 -2
- package/src/config.js +29 -0
- package/src/errors.js +53 -1
- package/src/features/discourse-tells.js +68 -0
- package/src/features/index.js +82 -8
- package/src/features/lexicon.js +40 -6
- package/src/features/markup-leakage.js +69 -0
- package/src/features/segment.js +41 -0
- package/src/features/signal-strength.js +81 -0
- package/src/features/stylometry.js +231 -1
- package/src/features/translationese.js +127 -0
- package/src/loader.js +76 -0
- package/src/logger.js +22 -23
- package/src/model-defaults.js +55 -0
- package/src/ouroboros.js +31 -0
- package/src/output.js +102 -90
- package/src/prompt-builder.js +103 -68
- package/src/providers.js +51 -4
- package/src/scoring.js +210 -2
- package/src/security.js +75 -0
- package/tests/fixtures/live-quality/en/public-docs-01.md +26 -0
- package/tests/fixtures/live-quality/ko/public-docs-01.md +26 -0
- package/tests/fixtures/suspect-zones/expected-ranges.json +207 -16
- package/tests/fixtures/suspect-zones/ja/ai/ja-ai-04-lexicon.md +11 -0
- package/tests/fixtures/suspect-zones/ja/natural/ja-nat-04-lexicon-cold.md +11 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-02.md +4 -5
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-07-ko-diagnostic.md +11 -0
- package/tests/fixtures/suspect-zones/zh/ai/zh-ai-04-lexicon.md +11 -0
- package/tests/fixtures/suspect-zones/zh/natural/zh-nat-04-lexicon-cold.md +11 -0
- package/tests/quality/README.md +188 -11
- package/tests/quality/adversarial-mps/fixtures.jsonl +10 -0
- package/tests/quality/benchmark.mjs +39 -1
- package/tests/quality/dogfood.mjs +5 -3
- package/tests/quality/live-fixtures.jsonl +2 -0
- package/tests/quality/live-quality.mjs +596 -0
- package/tests/quality/ranking-metrics.mjs +136 -0
- package/tests/quality/rebaseline-manifest.example.jsonl +5 -0
- package/vercel.json +53 -0
- package/SKILL-MAX.md +0 -455
- package/docs/internal/HARNESS.md +0 -14
- package/docs/internal/README.md +0 -14
- package/docs/internal/WARP.md +0 -23
- package/patina-max/SKILL.md +0 -523
- package/patina-max/composite.py +0 -457
- package/src/cache.js +0 -106
- package/src/commands/init.js +0 -208
- package/src/manifest.js +0 -162
- package/src/max-mode.js +0 -207
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
--bg: #070809;
|
|
4
|
+
--panel: #111318;
|
|
5
|
+
--panel-2: #171a21;
|
|
6
|
+
--text: #f5efe4;
|
|
7
|
+
--muted: #b8ad9a;
|
|
8
|
+
--line: #2a2d35;
|
|
9
|
+
--gold: #d8a948;
|
|
10
|
+
--amber: #f2c86b;
|
|
11
|
+
--green: #78d99a;
|
|
12
|
+
--red: #ff7b72;
|
|
13
|
+
--blue: #8ecaff;
|
|
14
|
+
--shadow: 0 24px 80px rgba(0, 0, 0, 0.42);
|
|
15
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
* { box-sizing: border-box; }
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
background:
|
|
24
|
+
radial-gradient(circle at 20% 10%, rgba(216, 169, 72, 0.18), transparent 34rem),
|
|
25
|
+
radial-gradient(circle at 80% 0%, rgba(142, 202, 255, 0.11), transparent 30rem),
|
|
26
|
+
linear-gradient(180deg, #08090b 0%, #101116 100%);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
a { color: var(--amber); text-decoration: none; }
|
|
31
|
+
a:hover { text-decoration: underline; }
|
|
32
|
+
|
|
33
|
+
.hero, .workspace, .footer {
|
|
34
|
+
width: min(1180px, calc(100% - 32px));
|
|
35
|
+
margin: 0 auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.hero { padding: 28px 0 22px; }
|
|
39
|
+
|
|
40
|
+
.nav, .panel__head, .button-row, .hero__actions, .cli-box__head {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.nav { justify-content: space-between; gap: 18px; }
|
|
46
|
+
|
|
47
|
+
.brand {
|
|
48
|
+
display: inline-flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 12px;
|
|
51
|
+
color: var(--text);
|
|
52
|
+
font-weight: 800;
|
|
53
|
+
letter-spacing: -0.04em;
|
|
54
|
+
font-size: 1.25rem;
|
|
55
|
+
}
|
|
56
|
+
.brand img { display: block; filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.35)); }
|
|
57
|
+
|
|
58
|
+
.nav__links { display: flex; gap: 18px; font-size: 0.95rem; }
|
|
59
|
+
|
|
60
|
+
.hero__copy {
|
|
61
|
+
padding: 78px 0 46px;
|
|
62
|
+
max-width: 850px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.eyebrow {
|
|
66
|
+
color: var(--gold);
|
|
67
|
+
font-weight: 800;
|
|
68
|
+
letter-spacing: 0.12em;
|
|
69
|
+
text-transform: uppercase;
|
|
70
|
+
font-size: 0.75rem;
|
|
71
|
+
margin: 0 0 10px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
h1, h2, h3, p { margin-top: 0; }
|
|
75
|
+
h1 {
|
|
76
|
+
font-size: clamp(2.6rem, 8vw, 6.2rem);
|
|
77
|
+
line-height: 0.92;
|
|
78
|
+
letter-spacing: -0.08em;
|
|
79
|
+
margin-bottom: 24px;
|
|
80
|
+
}
|
|
81
|
+
h2 { font-size: 1.2rem; margin-bottom: 0; }
|
|
82
|
+
h3 { font-size: 0.95rem; margin-bottom: 0; }
|
|
83
|
+
|
|
84
|
+
.hero__copy > p:not(.eyebrow) {
|
|
85
|
+
color: var(--muted);
|
|
86
|
+
font-size: clamp(1.05rem, 2vw, 1.3rem);
|
|
87
|
+
line-height: 1.7;
|
|
88
|
+
max-width: 720px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.hero__actions { gap: 10px; flex-wrap: wrap; margin-top: 22px; }
|
|
92
|
+
|
|
93
|
+
.workspace {
|
|
94
|
+
display: grid;
|
|
95
|
+
grid-template-columns: minmax(0, 1.15fr) minmax(340px, 0.85fr);
|
|
96
|
+
gap: 18px;
|
|
97
|
+
align-items: start;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.panel {
|
|
101
|
+
background: color-mix(in srgb, var(--panel) 92%, transparent);
|
|
102
|
+
border: 1px solid var(--line);
|
|
103
|
+
border-radius: 24px;
|
|
104
|
+
padding: 22px;
|
|
105
|
+
box-shadow: var(--shadow);
|
|
106
|
+
backdrop-filter: blur(18px);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.panel.full { grid-column: 1 / -1; }
|
|
110
|
+
.panel__head { justify-content: space-between; gap: 16px; margin-bottom: 18px; }
|
|
111
|
+
|
|
112
|
+
.field { display: grid; gap: 6px; color: var(--muted); font-size: 0.8rem; font-weight: 700; }
|
|
113
|
+
select, textarea, button {
|
|
114
|
+
font: inherit;
|
|
115
|
+
}
|
|
116
|
+
select {
|
|
117
|
+
color: var(--text);
|
|
118
|
+
background: var(--panel-2);
|
|
119
|
+
border: 1px solid var(--line);
|
|
120
|
+
border-radius: 12px;
|
|
121
|
+
padding: 10px 34px 10px 12px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
textarea {
|
|
125
|
+
width: 100%;
|
|
126
|
+
min-height: 330px;
|
|
127
|
+
resize: vertical;
|
|
128
|
+
color: var(--text);
|
|
129
|
+
background: #0b0d11;
|
|
130
|
+
border: 1px solid var(--line);
|
|
131
|
+
border-radius: 18px;
|
|
132
|
+
padding: 18px;
|
|
133
|
+
line-height: 1.65;
|
|
134
|
+
outline: none;
|
|
135
|
+
}
|
|
136
|
+
textarea:focus, select:focus, button:focus-visible, a:focus-visible {
|
|
137
|
+
outline: 2px solid var(--amber);
|
|
138
|
+
outline-offset: 3px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.button-row { gap: 10px; flex-wrap: wrap; margin-top: 14px; }
|
|
142
|
+
.button {
|
|
143
|
+
border: 0;
|
|
144
|
+
border-radius: 999px;
|
|
145
|
+
padding: 11px 18px;
|
|
146
|
+
color: #111;
|
|
147
|
+
background: linear-gradient(135deg, var(--amber), #f9e7a6);
|
|
148
|
+
font-weight: 850;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
}
|
|
151
|
+
.button.secondary, .button.ghost {
|
|
152
|
+
color: var(--text);
|
|
153
|
+
background: #1d2028;
|
|
154
|
+
border: 1px solid var(--line);
|
|
155
|
+
}
|
|
156
|
+
.button:hover { transform: translateY(-1px); }
|
|
157
|
+
|
|
158
|
+
.status, .quiet, .muted {
|
|
159
|
+
color: var(--muted);
|
|
160
|
+
}
|
|
161
|
+
.status { min-height: 1.4em; margin: 10px 0 0; }
|
|
162
|
+
|
|
163
|
+
.score-band, .pill {
|
|
164
|
+
border-radius: 999px;
|
|
165
|
+
padding: 7px 10px;
|
|
166
|
+
font-size: 0.8rem;
|
|
167
|
+
font-weight: 800;
|
|
168
|
+
}
|
|
169
|
+
.score-band[data-tone="good"], .pill.clean { color: #10351e; background: var(--green); }
|
|
170
|
+
.score-band[data-tone="warn"] { color: #3d2a00; background: var(--amber); }
|
|
171
|
+
.score-band[data-tone="hot"], .pill.hot { color: #3f0c08; background: var(--red); }
|
|
172
|
+
|
|
173
|
+
.score-meter { display: grid; gap: 18px; }
|
|
174
|
+
.score-meter__number { font-size: 5rem; line-height: 1; letter-spacing: -0.08em; font-weight: 900; }
|
|
175
|
+
.score-meter__number small { color: var(--muted); font-size: 1.3rem; letter-spacing: 0; }
|
|
176
|
+
.score-meter__bar {
|
|
177
|
+
height: 14px;
|
|
178
|
+
border-radius: 999px;
|
|
179
|
+
background: #0b0d11;
|
|
180
|
+
border: 1px solid var(--line);
|
|
181
|
+
overflow: hidden;
|
|
182
|
+
}
|
|
183
|
+
.score-meter__bar span {
|
|
184
|
+
--score: 0%;
|
|
185
|
+
display: block;
|
|
186
|
+
height: 100%;
|
|
187
|
+
width: var(--score);
|
|
188
|
+
background: linear-gradient(90deg, var(--green), var(--amber), var(--red));
|
|
189
|
+
transition: width 180ms ease;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.summary { display: grid; gap: 8px; padding-left: 1.1rem; color: var(--muted); }
|
|
193
|
+
.summary strong { color: var(--text); }
|
|
194
|
+
|
|
195
|
+
.cli-box {
|
|
196
|
+
margin-top: 20px;
|
|
197
|
+
border: 1px solid var(--line);
|
|
198
|
+
border-radius: 18px;
|
|
199
|
+
background: #0b0d11;
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
}
|
|
202
|
+
.cli-box__head {
|
|
203
|
+
justify-content: space-between;
|
|
204
|
+
gap: 12px;
|
|
205
|
+
padding: 12px 14px;
|
|
206
|
+
border-bottom: 1px solid var(--line);
|
|
207
|
+
color: var(--muted);
|
|
208
|
+
}
|
|
209
|
+
.cli-box pre {
|
|
210
|
+
margin: 0;
|
|
211
|
+
padding: 14px;
|
|
212
|
+
max-height: 220px;
|
|
213
|
+
overflow: auto;
|
|
214
|
+
color: #f5efe4;
|
|
215
|
+
white-space: pre-wrap;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.table-wrap { overflow-x: auto; }
|
|
219
|
+
table { width: 100%; border-collapse: collapse; min-width: 850px; }
|
|
220
|
+
th, td { padding: 12px; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
221
|
+
th { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.08em; }
|
|
222
|
+
|
|
223
|
+
.diff-list { display: grid; gap: 14px; }
|
|
224
|
+
.diff-card {
|
|
225
|
+
border: 1px solid var(--line);
|
|
226
|
+
border-radius: 18px;
|
|
227
|
+
background: #0b0d11;
|
|
228
|
+
padding: 16px;
|
|
229
|
+
}
|
|
230
|
+
.diff-card.hot { border-color: color-mix(in srgb, var(--red) 60%, var(--line)); }
|
|
231
|
+
.diff-card.clean { border-color: color-mix(in srgb, var(--green) 45%, var(--line)); }
|
|
232
|
+
.diff-card__head { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 10px; color: var(--muted); }
|
|
233
|
+
.diff-card p { line-height: 1.75; }
|
|
234
|
+
.diff-card ul { color: var(--muted); padding-left: 1.2rem; }
|
|
235
|
+
.diff-card code {
|
|
236
|
+
display: inline-block;
|
|
237
|
+
margin: 0 4px 6px 0;
|
|
238
|
+
padding: 3px 7px;
|
|
239
|
+
border: 1px solid var(--line);
|
|
240
|
+
border-radius: 999px;
|
|
241
|
+
color: var(--amber);
|
|
242
|
+
background: #151820;
|
|
243
|
+
}
|
|
244
|
+
mark {
|
|
245
|
+
color: #140f05;
|
|
246
|
+
background: var(--amber);
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
padding: 0 4px;
|
|
249
|
+
}
|
|
250
|
+
.empty-state {
|
|
251
|
+
color: var(--muted);
|
|
252
|
+
border: 1px dashed var(--line);
|
|
253
|
+
border-radius: 16px;
|
|
254
|
+
padding: 18px;
|
|
255
|
+
}
|
|
256
|
+
.footer {
|
|
257
|
+
padding: 34px 0 48px;
|
|
258
|
+
color: var(--muted);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@media (max-width: 880px) {
|
|
262
|
+
.workspace { grid-template-columns: 1fr; }
|
|
263
|
+
.nav { align-items: flex-start; }
|
|
264
|
+
.nav__links { flex-wrap: wrap; justify-content: flex-end; }
|
|
265
|
+
.panel__head { align-items: flex-start; flex-direction: column; }
|
|
266
|
+
.score-meter__number { font-size: 4rem; }
|
|
267
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
profile: namuwiki
|
|
3
|
+
name: 나무위키풍 프로필
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
scope: 한국어 위키풍 설명, 인터넷 커뮤니티식 부연, 가벼운 리뷰/해설 글. ko 전용이며 실제 나무위키 문서 텍스트를 포함하지 않는다.
|
|
6
|
+
language: ko
|
|
7
|
+
license-note: Original style description only; do not copy CC BY-NC-SA NamuWiki article text into this repo.
|
|
8
|
+
voice-overrides:
|
|
9
|
+
first-person: normal
|
|
10
|
+
opinions: amplify
|
|
11
|
+
rhythm-variation: amplify
|
|
12
|
+
humor: allow
|
|
13
|
+
messiness: amplify
|
|
14
|
+
concrete-emotions: amplify
|
|
15
|
+
structure-heavy: reduce
|
|
16
|
+
pattern-overrides:
|
|
17
|
+
ko:
|
|
18
|
+
# current schema: suppress=allow this marker, reduce=only fix overuse, amplify=fix aggressively
|
|
19
|
+
13: reduce # 연결 표현: 후술/상술 같은 위키체 접속은 일부 허용, 반복만 정리
|
|
20
|
+
14: reduce # 볼드체: 표제어/주의 강조는 일부 허용, 기계적 전면 볼드만 교정
|
|
21
|
+
15: reduce # 인라인 헤더: 목록+잡담 혼합 문체에서 일부 허용
|
|
22
|
+
17: reduce # 이모지/기호: 원문에 있는 가벼운 장식은 일부 허용
|
|
23
|
+
22: reduce # 채움 표현: 사족과 말맛은 보존, 빈 완충어 남발만 줄임
|
|
24
|
+
23: reduce # 헤징: '~인 듯', '~카더라' 류는 장르 표식일 수 있음
|
|
25
|
+
24: suppress # 막연한 긍정 결론: 위키풍 글은 닫힌 홍보 결론보다 열린 사족이 자연스러움
|
|
26
|
+
25: reduce # 구조 반복: 목록은 허용하되 문장 틀 복붙은 교정
|
|
27
|
+
26: reduce # 번역체: 일부 어색함/구어 리듬은 보존, 영어 직역만 교정
|
|
28
|
+
30: reduce # 수사적 질문: 문단 도입 드립은 일부 허용
|
|
29
|
+
31: suppress # 결론 신호어: '결론적으로'식 마무리보다 문단 자체로 끝내기
|
|
30
|
+
1: amplify # 거창한 중요성 부여는 위키풍이 아니라 AI/홍보 톤이므로 더 엄격히 제거
|
|
31
|
+
4: amplify # 홍보성 언어는 위키풍과 충돌
|
|
32
|
+
7: amplify # AI 고빈도 어휘는 인터넷식 말맛을 죽임
|
|
33
|
+
8: amplify # '~적' 남발은 위키풍보다 보고서투에 가까움
|
|
34
|
+
10: amplify # 3의 법칙은 위키풍 사족보다 발표자료 톤
|
|
35
|
+
16: amplify # '~고 있다' 진행형 남발은 교정
|
|
36
|
+
18: amplify # 과도한 한자어/공식어는 위키풍 구어성을 해침
|
|
37
|
+
19: suppress # 챗봇 응대 표현은 나무위키풍으로 바꾸기보다 제거
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# 나무위키풍 프로필
|
|
41
|
+
|
|
42
|
+
이 프로필은 한국어 글을 **위키풍 해설 + 인터넷식 사족** 쪽으로 기울인다. 목적은 실제 나무위키 문서를 모사하거나 복제하는 것이 아니라, 딱딱한 AI 문체를 한국어 인터넷 독자가 익숙하게 읽는 설명체로 낮추는 것이다.
|
|
43
|
+
|
|
44
|
+
## 라이선스 가드레일
|
|
45
|
+
|
|
46
|
+
- 실제 나무위키 문서 문장, 예시, 제목, 각주를 복사하지 않는다.
|
|
47
|
+
- 예시는 모두 이 저장소에서 새로 쓴 문장이어야 한다.
|
|
48
|
+
- 외부 덤프는 오프라인 통계 확인에만 쓸 수 있고, 텍스트는 repo에 반입하지 않는다.
|
|
49
|
+
|
|
50
|
+
## 적용 범위
|
|
51
|
+
|
|
52
|
+
- 한국어 글(`--lang ko`) 전용.
|
|
53
|
+
- 짧은 제품 해설, 게임/도구 리뷰, 커뮤니티 공지, 가벼운 기술 소개에 적합하다.
|
|
54
|
+
- 논문, 법률/의학 안내, 회사 공식 공지에는 쓰지 않는다. 이런 글은 `academic`, `legal`, `medical`, `formal`이 맞다.
|
|
55
|
+
|
|
56
|
+
## 어조 지침
|
|
57
|
+
|
|
58
|
+
- **표제어 설명처럼 시작한다.** 첫 문장은 대상을 짧게 정의하고, 바로 예외나 맥락을 붙인다.
|
|
59
|
+
- **사족을 허용한다.** 괄호 부연, 짧은 딴지, `[* ...]` 형식의 주석을 쓸 수 있다. 단, 사실을 새로 만들지 않는다.
|
|
60
|
+
- **완벽한 중립을 강요하지 않는다.** 원문에 평가가 있으면 살리고, 너무 홍보 문장처럼 보이면 한 발 물러서서 쓴다.
|
|
61
|
+
- **목록과 산문을 섞는다.** 모든 단락을 같은 길이와 같은 구조로 맞추지 않는다.
|
|
62
|
+
- **구어와 문어를 섞는다.** `~다`체를 기본으로 하되, `다만`, `문제는`, `그렇다고` 같은 구어적 연결을 허용한다.
|
|
63
|
+
- **드립은 작게.** 농담은 문장 하나 안에서 끝내고, 핵심 설명을 가리지 않는다.
|
|
64
|
+
|
|
65
|
+
## 적극 보존 / 허용
|
|
66
|
+
|
|
67
|
+
- 괄호 부연: `(물론 이것만으로 해결되진 않는다)`
|
|
68
|
+
- 각주풍 사족: `[* 실제로는 설정 파일 한 줄 때문에 터지는 경우가 많다.]`
|
|
69
|
+
- 위키체 연결: `후술`, `상술한`, `전술한`, `~인 셈`, `~카더라` — 과하면 줄인다.
|
|
70
|
+
- 짧은 평가: `애매하다`, `귀찮다`, `생각보다 세다`처럼 독자가 감을 잡는 말.
|
|
71
|
+
|
|
72
|
+
## 계속 제거할 AI 티
|
|
73
|
+
|
|
74
|
+
- `혁신적인`, `강력한`, `효율적인` 같은 빈 수식어.
|
|
75
|
+
- `중요한 역할을 하고 있다`, `미래가 기대된다` 같은 닫힌 홍보 결론.
|
|
76
|
+
- 모든 문단이 `정의 → 장점 → 결론`으로 반복되는 구조.
|
|
77
|
+
- `~적` 형용사와 한자어가 한 문장에 몰리는 보고서투.
|
|
78
|
+
|
|
79
|
+
## Before / After
|
|
80
|
+
|
|
81
|
+
### 도구 소개
|
|
82
|
+
|
|
83
|
+
**Before**
|
|
84
|
+
```text
|
|
85
|
+
이 솔루션은 혁신적인 접근을 통해 사용자의 생산성을 극대화하고, 체계적인 워크플로우를 기반으로 지속 가능한 가치를 제공합니다.
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**After**
|
|
89
|
+
```text
|
|
90
|
+
작업 문장을 한 번 훑어서 AI 티 나는 부분을 표시해 주는 도구다. 생산성을 올린다고 크게 말할 수도 있겠지만, 실제로는 `혁신적인`, `체계적인` 같은 말부터 지우는 쪽에 가깝다.[* 그래서 결과물이 더 수수해진다. 이게 장점이다.]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 기능 설명
|
|
94
|
+
|
|
95
|
+
**Before**
|
|
96
|
+
```text
|
|
97
|
+
본 기능은 다양한 사용 환경에서 효과적으로 활용될 수 있으며, 향후 사용자 경험 향상에 중요한 역할을 할 것으로 기대됩니다.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**After**
|
|
101
|
+
```text
|
|
102
|
+
브라우저에서는 점수만 보고, 실제 재작성은 CLI나 에디터 스킬에서 한다. 역할이 나뉘어 있어서 처음엔 조금 헷갈릴 수 있다. 대신 웹에 글을 맡기는 구조가 아니라는 점은 꽤 명확하다.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 사용
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
patina --lang ko --profile namuwiki input.txt
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`--lang en|zh|ja`와 함께 쓰면 기본 프로필로 폴백한다. 이 프로필은 한국어 조사, 어미, 인터넷식 부연을 전제로 하기 때문이다.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve, relative } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { analyzeText } from '../src/features/index.js';
|
|
7
|
+
import { loadLexicon } from '../src/features/lexicon.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const repoRoot = resolve(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_INPUT = 'tests/quality/adversarial-mps/fixtures.jsonl';
|
|
13
|
+
export const DEFAULT_OUTPUT = 'docs/research/adversarial-mps.md';
|
|
14
|
+
|
|
15
|
+
export function parseArgs(argv = process.argv.slice(2)) {
|
|
16
|
+
const args = { input: DEFAULT_INPUT, output: DEFAULT_OUTPUT, json: false, check: false };
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const arg = argv[i];
|
|
19
|
+
if (arg === '--input') args.input = argv[++i];
|
|
20
|
+
else if (arg === '--output') args.output = argv[++i];
|
|
21
|
+
else if (arg === '--json') args.json = true;
|
|
22
|
+
else if (arg === '--check') args.check = true;
|
|
23
|
+
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
24
|
+
else throw new Error(`unknown option ${arg}`);
|
|
25
|
+
}
|
|
26
|
+
return args;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadFixtures(path = DEFAULT_INPUT) {
|
|
30
|
+
const fullPath = resolve(repoRoot, path);
|
|
31
|
+
if (!existsSync(fullPath)) throw new Error(`fixture file not found: ${path}`);
|
|
32
|
+
return readFileSync(fullPath, 'utf8')
|
|
33
|
+
.split('\n')
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((line, index) => {
|
|
37
|
+
try {
|
|
38
|
+
return normalizeFixture(JSON.parse(line));
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw new Error(`${path}:${index + 1}: ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeFixture(row) {
|
|
46
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) throw new Error('fixture must be an object');
|
|
47
|
+
for (const key of ['id', 'lang', 'original', 'rewritten']) {
|
|
48
|
+
if (typeof row[key] !== 'string' || row[key].trim() === '') throw new Error(`${key} is required`);
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(row.anchors) || row.anchors.length === 0) throw new Error('anchors must be a non-empty array');
|
|
51
|
+
const anchors = row.anchors.map((anchor) => {
|
|
52
|
+
if (typeof anchor !== 'string' || anchor.trim() === '') throw new Error('anchors must be non-empty strings');
|
|
53
|
+
return anchor.trim();
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
expected_mps_min: 90,
|
|
57
|
+
expected_ai_min: 60,
|
|
58
|
+
...row,
|
|
59
|
+
anchors,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function evaluateFixtures(fixtures) {
|
|
64
|
+
return fixtures.map((fixture) => {
|
|
65
|
+
const mps = anchorMps(fixture);
|
|
66
|
+
const ai = deterministicAiScore(fixture.rewritten, fixture.lang);
|
|
67
|
+
const pass = mps.mps >= fixture.expected_mps_min && ai.score >= fixture.expected_ai_min;
|
|
68
|
+
return { ...fixture, mps, ai, pass };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function anchorMps({ rewritten, anchors }) {
|
|
73
|
+
const haystack = normalizeText(rewritten);
|
|
74
|
+
const checked = anchors.map((anchor) => ({
|
|
75
|
+
anchor,
|
|
76
|
+
pass: haystack.includes(normalizeText(anchor)),
|
|
77
|
+
}));
|
|
78
|
+
const passCount = checked.filter((item) => item.pass).length;
|
|
79
|
+
const totalCount = checked.length;
|
|
80
|
+
return {
|
|
81
|
+
pass_count: passCount,
|
|
82
|
+
total_count: totalCount,
|
|
83
|
+
mps: totalCount ? round1((passCount / totalCount) * 100) : 0,
|
|
84
|
+
anchors: checked,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deterministicAiScore(text, lang) {
|
|
89
|
+
const result = analyzeText(text, {
|
|
90
|
+
lang,
|
|
91
|
+
repoRoot,
|
|
92
|
+
lexicon: loadLexicon(lang, repoRoot),
|
|
93
|
+
});
|
|
94
|
+
const paragraphs = Array.isArray(result.paragraphs) ? result.paragraphs : [];
|
|
95
|
+
const paragraphCount = paragraphs.length;
|
|
96
|
+
const hotParagraphs = paragraphs.filter((p) => p.hot).length;
|
|
97
|
+
return {
|
|
98
|
+
score: paragraphCount ? round1((hotParagraphs / paragraphCount) * 100) : 0,
|
|
99
|
+
paragraph_count: paragraphCount,
|
|
100
|
+
hot_paragraphs: hotParagraphs,
|
|
101
|
+
lexicon_hits: Array.from(new Set(paragraphs.flatMap((p) => p.lexicon?.hits || []))).sort(),
|
|
102
|
+
ko_diagnostics_hot: paragraphs.filter((p) => p.koDiagnostics?.hot).length,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeText(text) {
|
|
107
|
+
return String(text || '').normalize('NFC').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function round1(n) {
|
|
111
|
+
return Math.round(n * 10) / 10;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function summarize(rows) {
|
|
115
|
+
const total = rows.length;
|
|
116
|
+
const passing = rows.filter((r) => r.pass).length;
|
|
117
|
+
return {
|
|
118
|
+
total,
|
|
119
|
+
passing,
|
|
120
|
+
failing: total - passing,
|
|
121
|
+
min_mps: rows.length ? Math.min(...rows.map((r) => r.mps.mps)) : 0,
|
|
122
|
+
min_ai: rows.length ? Math.min(...rows.map((r) => r.ai.score)) : 0,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function formatMarkdown(rows, { input = DEFAULT_INPUT } = {}) {
|
|
127
|
+
const summary = summarize(rows);
|
|
128
|
+
const lines = [];
|
|
129
|
+
lines.push('# Adversarial MPS audit');
|
|
130
|
+
lines.push('');
|
|
131
|
+
lines.push('This report checks whether a rewrite can preserve explicit meaning anchors while still looking AI-like. It is a repo-owned adversarial fixture set, not a public model-performance claim.');
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push(`Fixture source: \`${input}\``);
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push('## Summary');
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push(`- Fixtures: ${summary.total}`);
|
|
138
|
+
lines.push(`- Passing adversarial cases: ${summary.passing}/${summary.total}`);
|
|
139
|
+
lines.push(`- Minimum anchor-MPS proxy: ${summary.min_mps.toFixed(1)}`);
|
|
140
|
+
lines.push(`- Minimum deterministic AI score: ${summary.min_ai.toFixed(1)}`);
|
|
141
|
+
lines.push('- Gate: MPS proxy ≥90 and deterministic AI score ≥60.');
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push('## Results');
|
|
144
|
+
lines.push('');
|
|
145
|
+
lines.push('| id | lang | register | MPS proxy | AI score | hot paragraphs | status |');
|
|
146
|
+
lines.push('|---|---|---|---:|---:|---:|---|');
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
lines.push(`| ${cell(row.id)} | ${cell(row.lang)} | ${cell(row.register || '')} | ${row.mps.mps.toFixed(1)} | ${row.ai.score.toFixed(1)} | ${row.ai.hot_paragraphs}/${row.ai.paragraph_count} | ${row.pass ? 'pass' : 'fail'} |`);
|
|
149
|
+
}
|
|
150
|
+
lines.push('');
|
|
151
|
+
lines.push('## Interpretation');
|
|
152
|
+
lines.push('');
|
|
153
|
+
lines.push('The audit confirms the known gap: an anchor-preservation floor can pass text that still retains AI-marker density. MPS should remain a meaning-safety floor, not a humanness score. A complementary anti-gaming check should penalize repeated AI-marker recurrence after rewrite, especially when MPS is high.');
|
|
154
|
+
lines.push('');
|
|
155
|
+
lines.push('## Proposed MPS-v2 companion check');
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push('Keep MPS unchanged for semantic safety, then add an independent recurrence gate:');
|
|
158
|
+
lines.push('');
|
|
159
|
+
lines.push('1. Score the original and rewritten text with deterministic `analyzeText`.');
|
|
160
|
+
lines.push('2. If `MPS ≥ 90` and rewritten AI score remains `≥ 60`, mark the candidate as `style_not_improved`.');
|
|
161
|
+
lines.push('3. In Ouroboros selection, prefer candidates that pass MPS and lower the AI score; do not let high MPS alone rescue a visibly AI-like rewrite.');
|
|
162
|
+
lines.push('4. Report preserved anchors and recurring AI markers separately so users can decide whether to edit more or keep the register.');
|
|
163
|
+
return `${lines.join('\n')}\n`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function cell(value) {
|
|
167
|
+
return String(value ?? '').replace(/\|/g, '\\|').replace(/\n/g, '<br>');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function writeReport(rows, { output = DEFAULT_OUTPUT, input = DEFAULT_INPUT } = {}) {
|
|
171
|
+
const outPath = resolve(repoRoot, output);
|
|
172
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
173
|
+
const markdown = formatMarkdown(rows, { input });
|
|
174
|
+
writeFileSync(outPath, markdown);
|
|
175
|
+
return { path: relative(repoRoot, outPath), markdown };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function printHelp() {
|
|
179
|
+
console.log(`Usage: node scripts/adversarial-mps-report.mjs [--input <fixtures.jsonl>] [--output <report.md>] [--check] [--json]\n\nValidates hand-built adversarial MPS fixtures: MPS proxy >= 90 and deterministic AI score >= 60.`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
183
|
+
try {
|
|
184
|
+
const args = parseArgs();
|
|
185
|
+
if (args.help) {
|
|
186
|
+
printHelp();
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
const rows = evaluateFixtures(loadFixtures(args.input));
|
|
190
|
+
if (args.json) console.log(JSON.stringify({ summary: summarize(rows), rows }, null, 2));
|
|
191
|
+
else {
|
|
192
|
+
const result = writeReport(rows, { input: args.input, output: args.output });
|
|
193
|
+
console.log(`Wrote ${result.path}`);
|
|
194
|
+
console.log(JSON.stringify(summarize(rows)));
|
|
195
|
+
}
|
|
196
|
+
if (args.check && rows.some((row) => !row.pass)) process.exitCode = 1;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error(`adversarial-mps-report: ${err.message}`);
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { scoreFiles, summarizeRows } from './prose-score.mjs';
|
|
6
|
+
|
|
7
|
+
export function badgeBand(maxScore = 0) {
|
|
8
|
+
const score = Number(maxScore);
|
|
9
|
+
if (!Number.isFinite(score)) return { text: 'human-ish', color: 'brightgreen' };
|
|
10
|
+
if (score <= 30) return { text: 'human-ish', color: 'brightgreen' };
|
|
11
|
+
if (score <= 50) return { text: 'mixed', color: 'yellow' };
|
|
12
|
+
return { text: 'ai-like', color: 'red' };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatBadgeScore(maxScore = 0) {
|
|
16
|
+
const score = Number(maxScore);
|
|
17
|
+
return `${Math.round(Number.isFinite(score) ? score : 0)}%`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function toShieldsEndpoint(summary, { label = 'patina' } = {}) {
|
|
21
|
+
const maxScore = summary?.maxScore ?? 0;
|
|
22
|
+
const band = badgeBand(maxScore);
|
|
23
|
+
return {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
label,
|
|
26
|
+
message: `${formatBadgeScore(maxScore)} · ${band.text}`,
|
|
27
|
+
color: band.color,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseArgs(argv) {
|
|
32
|
+
const opts = {
|
|
33
|
+
files: [],
|
|
34
|
+
gate: 30,
|
|
35
|
+
lang: 'auto',
|
|
36
|
+
maxFiles: 50,
|
|
37
|
+
label: 'patina',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < argv.length; i++) {
|
|
41
|
+
const arg = argv[i];
|
|
42
|
+
if (arg === '--score-threshold') opts.gate = Number(argv[++i]);
|
|
43
|
+
else if (arg === '--lang') opts.lang = argv[++i] || 'auto';
|
|
44
|
+
else if (arg === '--max-files') opts.maxFiles = Number(argv[++i]);
|
|
45
|
+
else if (arg === '--label') opts.label = argv[++i] || 'patina';
|
|
46
|
+
else if (arg.startsWith('-')) throw new Error(`unknown option ${arg}`);
|
|
47
|
+
else opts.files.push(arg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (opts.files.length === 0) opts.files.push('README.md');
|
|
51
|
+
if (!Number.isFinite(opts.gate) || opts.gate < 0 || opts.gate > 100) {
|
|
52
|
+
throw new Error(`--score-threshold expects a number from 0 to 100, got ${opts.gate}`);
|
|
53
|
+
}
|
|
54
|
+
if (!Number.isInteger(opts.maxFiles) || opts.maxFiles < 1) {
|
|
55
|
+
throw new Error(`--max-files expects a positive integer, got ${opts.maxFiles}`);
|
|
56
|
+
}
|
|
57
|
+
return opts;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildBadge(files, opts = {}) {
|
|
61
|
+
const rows = scoreFiles(files, opts);
|
|
62
|
+
return toShieldsEndpoint(summarizeRows(rows), opts);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function run(argv = process.argv.slice(2)) {
|
|
66
|
+
const opts = parseArgs(argv);
|
|
67
|
+
const badge = buildBadge(opts.files, opts);
|
|
68
|
+
process.stdout.write(`${JSON.stringify(badge)}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const directPath = process.argv[1] ? resolve(process.argv[1]) : '';
|
|
72
|
+
if (directPath === fileURLToPath(import.meta.url)) {
|
|
73
|
+
try {
|
|
74
|
+
run();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`patina-badge: ${error.message}`);
|
|
77
|
+
process.exitCode = 2;
|
|
78
|
+
}
|
|
79
|
+
}
|