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
|
@@ -43,14 +43,17 @@ function readResults() {
|
|
|
43
43
|
|
|
44
44
|
function validateResultsSchema(results) {
|
|
45
45
|
const missing = [];
|
|
46
|
-
if (results?.schemaVersion !==
|
|
46
|
+
if (results?.schemaVersion !== 3) missing.push('schemaVersion=3');
|
|
47
47
|
if (typeof results?.fixtureSchemaVersion !== 'number') missing.push('fixtureSchemaVersion');
|
|
48
48
|
if (typeof results?.nodeVersion !== 'string') missing.push('nodeVersion');
|
|
49
49
|
if (typeof results?.overall?.ci_low !== 'number') missing.push('overall.ci_low');
|
|
50
50
|
if (typeof results?.overall?.ci_high !== 'number') missing.push('overall.ci_high');
|
|
51
51
|
if (typeof results?.overall?.n !== 'number') missing.push('overall.n');
|
|
52
|
+
if (!isNumberOrNull(results?.ranking?.overall?.roc_auc)) missing.push('ranking.overall.roc_auc');
|
|
53
|
+
if (!isNumberOrNull(results?.ranking?.overall?.pr_auc)) missing.push('ranking.overall.pr_auc');
|
|
54
|
+
if (!results?.ranking?.overall?.bestF1) missing.push('ranking.overall.bestF1');
|
|
52
55
|
for (const [lang, summary] of Object.entries(results?.perLanguage || {})) {
|
|
53
|
-
for (const detector of ['burstiness', 'mattr', 'lexicon']) {
|
|
56
|
+
for (const detector of ['burstiness', 'koDiagnostics', 'mattr', 'lexicon']) {
|
|
54
57
|
if (!summary.byDetector?.[detector]) missing.push(`perLanguage.${lang}.byDetector.${detector}`);
|
|
55
58
|
}
|
|
56
59
|
}
|
|
@@ -59,6 +62,10 @@ function validateResultsSchema(results) {
|
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
function isNumberOrNull(value) {
|
|
66
|
+
return value === null || typeof value === 'number';
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
function pct(value) {
|
|
63
70
|
return `${((value ?? 0) * 100).toFixed(1)}%`;
|
|
64
71
|
}
|
|
@@ -67,6 +74,14 @@ function num(value, digits = 3) {
|
|
|
67
74
|
return Number(value ?? 0).toFixed(digits).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
function optionalPct(value) {
|
|
78
|
+
return value === null || value === undefined ? '—' : pct(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function optionalNum(value, digits = 3) {
|
|
82
|
+
return value === null || value === undefined ? '—' : num(value, digits);
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
function bool(value) {
|
|
71
86
|
return value ? 'hot' : 'cold';
|
|
72
87
|
}
|
|
@@ -103,6 +118,18 @@ function detectorRows(perLanguage = {}) {
|
|
|
103
118
|
return rows;
|
|
104
119
|
}
|
|
105
120
|
|
|
121
|
+
function rankingRows(ranking = {}) {
|
|
122
|
+
const rows = [];
|
|
123
|
+
if (ranking.overall) rows.push(['overall', ranking.overall]);
|
|
124
|
+
for (const [lang, summary] of Object.entries(ranking.perLanguage || {}).sort(([a], [b]) => a.localeCompare(b))) {
|
|
125
|
+
rows.push([lang, summary]);
|
|
126
|
+
}
|
|
127
|
+
return rows.map(([scope, summary]) => {
|
|
128
|
+
const best = summary.bestF1 || {};
|
|
129
|
+
return `| ${cell(scope)} | ${summary.n} | ${summary.positives} | ${summary.negatives} | ${optionalNum(summary.roc_auc)} | ${optionalNum(summary.pr_auc)} | ${optionalNum(best.threshold)} | ${optionalPct(best.precision)} | ${optionalPct(best.recall)} | ${optionalNum(best.f1, 2)} | ${optionalPct(best.accuracy)} |`;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
106
133
|
function classRows(fixtures = []) {
|
|
107
134
|
const counts = new Map();
|
|
108
135
|
for (const f of fixtures) {
|
|
@@ -130,7 +157,10 @@ function sampleSizeSummary(fixtures = []) {
|
|
|
130
157
|
function fixtureRows(fixtures = []) {
|
|
131
158
|
return fixtures.map((f) => {
|
|
132
159
|
const hits = cell((f.lexicon_hits || []).slice(0, 4).join(', '));
|
|
133
|
-
|
|
160
|
+
const koDiag = f.ko_diagnostics_hot
|
|
161
|
+
? `hot: ${(f.ko_diagnostics_reasons || []).join(', ')}`
|
|
162
|
+
: 'cold';
|
|
163
|
+
return `| ${cell(f.fixture_id)} | ${cell(f.lang)} | ${cell(f.class)} | ${bool(f.expected_hot)} | ${bool(f.predicted_hot)} | ${resultMark(f.correct)} | ${num(f.signal_score)} | ${num(f.cv)} ${cell(f.cv_band)} | ${num(f.mattr)} ${cell(f.mattr_band)} | ${num(f.lexicon_density)} | ${cell(koDiag)} | ${hits} |`;
|
|
134
164
|
});
|
|
135
165
|
}
|
|
136
166
|
|
|
@@ -180,7 +210,7 @@ This is the latest checked-in report for patina's deterministic suspect-zone ben
|
|
|
180
210
|
- Regression ranges: \`tests/fixtures/suspect-zones/expected-ranges.json\` (refresh with \`npm run benchmark:ranges\`)
|
|
181
211
|
- Reproduce: \`npm run benchmark:report\`
|
|
182
212
|
- Raw JSON: [latest.json](latest.json)
|
|
183
|
-
- Detector comparison
|
|
213
|
+
- Detector comparison protocol: [detector-comparison.md](detector-comparison.md)
|
|
184
214
|
- 2025+ re-baseline plan: [docs/research/2025-rebaseline-plan.md](../research/2025-rebaseline-plan.md)
|
|
185
215
|
|
|
186
216
|
## Language breakdown
|
|
@@ -195,6 +225,16 @@ ${languageRows(results.perLanguage).join('\n')}
|
|
|
195
225
|
|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
196
226
|
${detectorRows(results.perLanguage).join('\n')}
|
|
197
227
|
|
|
228
|
+
## Ranking diagnostics
|
|
229
|
+
|
|
230
|
+
Signal-score ranking shows whether the diagnostic \`signal_score\` separates hot
|
|
231
|
+
fixtures from natural fixtures before any threshold is chosen. It is computed
|
|
232
|
+
only on the checked-in fixture corpus and is not a broader model-era claim.
|
|
233
|
+
|
|
234
|
+
| scope | fixtures | positives | negatives | ROC-AUC | PR-AUC | best threshold | precision | recall | best F1 | accuracy |
|
|
235
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
236
|
+
${rankingRows(results.ranking).join('\n')}
|
|
237
|
+
|
|
198
238
|
## Sample sizes
|
|
199
239
|
|
|
200
240
|
| lang | class | fixtures |
|
|
@@ -207,14 +247,15 @@ ${misclassificationSection(results.fixtures)}
|
|
|
207
247
|
|
|
208
248
|
## Fixture log
|
|
209
249
|
|
|
210
|
-
| fixture | lang | class | expected | predicted | ok | CV band | MATTR band | lexicon/1k | sample lexicon hits |
|
|
211
|
-
|
|
250
|
+
| fixture | lang | class | expected | predicted | ok | signal | CV band | MATTR band | lexicon/1k | KO diagnostic | sample lexicon hits |
|
|
251
|
+
|---|---|---|---|---|---:|---:|---:|---:|---:|---|---|
|
|
212
252
|
${fixtureRows(results.fixtures).join('\n')}
|
|
213
253
|
|
|
214
254
|
## How to read this
|
|
215
255
|
|
|
216
|
-
- **Hot** means at least one deterministic signal crossed the benchmark threshold: low burstiness CV, low MATTR,
|
|
256
|
+
- **Hot** means at least one deterministic signal crossed the benchmark threshold: low burstiness CV, low MATTR, AI-lexicon density, or the conservative Korean diagnostic composite.
|
|
217
257
|
- **Cold** means the fixture did not cross those thresholds.
|
|
258
|
+
- **Signal** is the 0–100 diagnostic strength of the strongest deterministic trigger. It supports ranking diagnostics but does not replace the binary hot/cold regression gate.
|
|
218
259
|
- The report is meant for regression tracking and contributor discussion, not for authorship accusation.
|
|
219
260
|
- This deterministic corpus is intentionally small (${results.fixtureCount} fixtures across ${languageList}); do not treat 100% fixture accuracy as generalization to new models, genres, or edited AI text.
|
|
220
261
|
- Confidence intervals use Wilson score intervals for the checked-in fixture set; external threshold sweeps and 2025+ model rebaselines are separate research follow-ups tracked in [2025+ Re-baseline Plan](../research/2025-rebaseline-plan.md).
|
|
@@ -230,7 +271,7 @@ function main() {
|
|
|
230
271
|
if (!runBenchmarkFirst) benchmarkStatus = statusFromResults(results);
|
|
231
272
|
|
|
232
273
|
const report = {
|
|
233
|
-
reportVersion:
|
|
274
|
+
reportVersion: 3,
|
|
234
275
|
benchmarkCommand: benchmarkCommand.join(' '),
|
|
235
276
|
benchmarkStatus,
|
|
236
277
|
note: 'Deterministic suspect-zone benchmark; not an authorship detector.',
|
|
@@ -249,4 +290,10 @@ function main() {
|
|
|
249
290
|
if (benchmarkStatus !== 0) process.exitCode = benchmarkStatus;
|
|
250
291
|
}
|
|
251
292
|
|
|
252
|
-
|
|
293
|
+
const isDirectRun = process.argv[1]
|
|
294
|
+
? resolve(process.cwd(), process.argv[1]) === fileURLToPath(import.meta.url)
|
|
295
|
+
: false;
|
|
296
|
+
|
|
297
|
+
if (isDirectRun) main();
|
|
298
|
+
|
|
299
|
+
export { renderMarkdown, validateResultsSchema };
|
|
@@ -10,8 +10,6 @@ expect(pkg.bin?.patina === 'bin/patina.js', 'package.json bin.patina must point
|
|
|
10
10
|
expect(pkg.bin?.['patina-score'] === 'scripts/precommit-score.mjs', 'package.json bin.patina-score must point to scripts/precommit-score.mjs');
|
|
11
11
|
expect(existsSync('bin/patina.js'), 'bin/patina.js must exist');
|
|
12
12
|
expect(readVersionField('SKILL.md') === version, 'SKILL.md version must match package.json');
|
|
13
|
-
expect(readVersionField('SKILL-MAX.md') === version, 'SKILL-MAX.md version must match package.json');
|
|
14
|
-
expect(readVersionField('patina-max/SKILL.md') === version, 'patina-max/SKILL.md version must match package.json');
|
|
15
13
|
expect(readVersionField('.patina.default.yaml') === version, '.patina.default.yaml version must match package.json');
|
|
16
14
|
expect(readFileSync('README.md', 'utf8').includes(`version: "${version}"`), 'README.md config example version must match package.json');
|
|
17
15
|
expect(new RegExp(`^## ${escapeRegex(version)} — \\d{4}-\\d{2}-\\d{2}`, 'm').test(readFileSync('CHANGELOG.md', 'utf8')), 'CHANGELOG.md must contain a release heading for package.json version');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Offline detector-comparison
|
|
2
|
+
// Offline detector-comparison protocol for the suspect-zone benchmark.
|
|
3
3
|
//
|
|
4
4
|
// Default mode compares Patina's deterministic in-tree analyzer against the
|
|
5
5
|
// checked-in fixture labels. Pass --input <manual-results.json> to merge scores
|
|
@@ -42,8 +42,8 @@ function runBenchmark() {
|
|
|
42
42
|
|
|
43
43
|
function readBenchmarkResults() {
|
|
44
44
|
const results = JSON.parse(readFileSync(RESULTS_PATH, 'utf8'));
|
|
45
|
-
if (results?.schemaVersion !==
|
|
46
|
-
throw new Error(`${relative(REPO_ROOT, RESULTS_PATH)} is not a benchmark schema
|
|
45
|
+
if (results?.schemaVersion !== 3 || !Array.isArray(results?.fixtures)) {
|
|
46
|
+
throw new Error(`${relative(REPO_ROOT, RESULTS_PATH)} is not a benchmark schema v3 result`);
|
|
47
47
|
}
|
|
48
48
|
return results;
|
|
49
49
|
}
|
|
@@ -121,7 +121,7 @@ function builtInDetector(results) {
|
|
|
121
121
|
name: 'Patina deterministic suspect-zone analyzer',
|
|
122
122
|
kind: 'in-tree',
|
|
123
123
|
mode: 'offline',
|
|
124
|
-
threshold: 'burstiness low OR MATTR low OR lexicon density > threshold',
|
|
124
|
+
threshold: 'burstiness low OR MATTR low OR lexicon density > threshold with min hits OR koDiagnostics hot',
|
|
125
125
|
},
|
|
126
126
|
],
|
|
127
127
|
rows: results.fixtures.map((fixture) => ({
|
|
@@ -205,9 +205,9 @@ function fixtureRows(rows) {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
function renderMarkdown(report) {
|
|
208
|
-
return `# Detector Comparison
|
|
208
|
+
return `# Detector Comparison Protocol
|
|
209
209
|
|
|
210
|
-
This report is generated offline from the checked-in suspect-zone fixtures. It is a comparison
|
|
210
|
+
This report is generated offline from the checked-in suspect-zone fixtures. It is a comparison protocol, not a vendor ranking claim.
|
|
211
211
|
|
|
212
212
|
## Current run
|
|
213
213
|
|
|
@@ -251,7 +251,7 @@ function main() {
|
|
|
251
251
|
generatedAt: new Date().toISOString(),
|
|
252
252
|
fixtureCount: results.fixtureCount,
|
|
253
253
|
benchmarkGeneratedAt: results.generatedAt,
|
|
254
|
-
note: 'Offline comparison
|
|
254
|
+
note: 'Offline comparison protocol. Built-in Patina row uses deterministic suspect-zone analyzer; third-party rows are manual opt-in only.',
|
|
255
255
|
manualInput: manual ? relative(REPO_ROOT, manual.path) : null,
|
|
256
256
|
detectors,
|
|
257
257
|
summaries: computed.summaries,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generate the browser-safe lexicon bundle for the static playground.
|
|
3
|
+
// Keep this dependency-free so Vercel can serve committed assets without a build.
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const REPO_ROOT = resolve(__dirname, '..');
|
|
11
|
+
const LANGS = ['en', 'ko', 'zh', 'ja'];
|
|
12
|
+
|
|
13
|
+
function parseFrontmatter(raw) {
|
|
14
|
+
if (!raw.startsWith('---')) return { meta: {}, body: raw };
|
|
15
|
+
const end = raw.indexOf('\n---', 3);
|
|
16
|
+
if (end === -1) return { meta: {}, body: raw };
|
|
17
|
+
const yaml = raw.slice(3, end).trim();
|
|
18
|
+
const meta = {};
|
|
19
|
+
for (const line of yaml.split('\n')) {
|
|
20
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/u);
|
|
21
|
+
if (match) meta[match[1]] = match[2].trim();
|
|
22
|
+
}
|
|
23
|
+
return { meta, body: raw.slice(end + 4) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseLexiconBody(body) {
|
|
27
|
+
const strict = [];
|
|
28
|
+
const phrases = [];
|
|
29
|
+
let mode = null;
|
|
30
|
+
for (const rawLine of body.split('\n')) {
|
|
31
|
+
const line = rawLine.trim();
|
|
32
|
+
if (line.startsWith('## ')) {
|
|
33
|
+
const heading = line.toLowerCase();
|
|
34
|
+
if (heading.includes('strict matches')) mode = 'strict';
|
|
35
|
+
else if (heading.includes('multi-word phrases')) mode = 'phrases';
|
|
36
|
+
else mode = null;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!mode || !line.startsWith('- ')) continue;
|
|
40
|
+
const entry = line.slice(2).trim().normalize('NFC');
|
|
41
|
+
if (entry && !entry.startsWith('_')) (mode === 'strict' ? strict : phrases).push(entry);
|
|
42
|
+
}
|
|
43
|
+
return { strict, phrases };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lexicons = {};
|
|
47
|
+
for (const lang of LANGS) {
|
|
48
|
+
const source = `lexicon/ai-${lang}.md`;
|
|
49
|
+
const raw = readFileSync(resolve(REPO_ROOT, source), 'utf8');
|
|
50
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
51
|
+
const parsed = parseLexiconBody(body);
|
|
52
|
+
lexicons[lang] = {
|
|
53
|
+
lang,
|
|
54
|
+
source,
|
|
55
|
+
version: meta.version || null,
|
|
56
|
+
strict: parsed.strict,
|
|
57
|
+
phrases: parsed.phrases,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const output = `// Generated by scripts/generate-playground-data.mjs. Do not edit by hand.\n` +
|
|
62
|
+
`// Source: lexicon/ai-{en,ko,zh,ja}.md\n\n` +
|
|
63
|
+
`export const PLAYGROUND_LEXICONS = ${JSON.stringify(lexicons, null, 2)};\n`;
|
|
64
|
+
|
|
65
|
+
const target = resolve(REPO_ROOT, 'playground/data/lexicons.js');
|
|
66
|
+
if (process.argv.includes('--check')) {
|
|
67
|
+
const current = readFileSync(target, 'utf8');
|
|
68
|
+
if (current !== output) {
|
|
69
|
+
console.error('playground/data/lexicons.js is stale; run npm run playground:data');
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
} else {
|
|
72
|
+
console.log('playground/data/lexicons.js is up to date');
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
writeFileSync(target, output, 'utf8');
|
|
76
|
+
console.log('Wrote playground/data/lexicons.js');
|
|
77
|
+
}
|