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
package/src/providers.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
// Provider presets: shortcuts for common OpenAI-compatible endpoints.
|
|
2
3
|
// Each provider maps to a base URL + a recommended default model + the env
|
|
3
4
|
// variable users typically set to authenticate. Selecting a provider is
|
|
4
5
|
// equivalent to manually setting --base-url, --model, and the right key.
|
|
5
6
|
import { inputError } from './errors.js';
|
|
7
|
+
import { DEFAULT_BEST_MODELS } from './model-defaults.js';
|
|
6
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Built-in OpenAI-compatible provider presets.
|
|
11
|
+
*
|
|
12
|
+
* @type {Record<string, {name: string, baseURL: string, apiKeyEnv: string, defaultModel: string, freeTier: boolean, note: string}>}
|
|
13
|
+
* @example
|
|
14
|
+
* const openaiBaseURL = PROVIDERS.openai.baseURL;
|
|
15
|
+
*/
|
|
7
16
|
export const PROVIDERS = {
|
|
8
17
|
openai: {
|
|
9
18
|
name: 'openai',
|
|
10
19
|
baseURL: 'https://api.openai.com/v1',
|
|
11
20
|
apiKeyEnv: 'OPENAI_API_KEY',
|
|
12
|
-
defaultModel:
|
|
21
|
+
defaultModel: DEFAULT_BEST_MODELS.openai,
|
|
13
22
|
freeTier: false,
|
|
14
23
|
note: 'Paid. Default OpenAI Platform API.',
|
|
15
24
|
},
|
|
@@ -17,7 +26,7 @@ export const PROVIDERS = {
|
|
|
17
26
|
name: 'gemini',
|
|
18
27
|
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
19
28
|
apiKeyEnv: 'GEMINI_API_KEY',
|
|
20
|
-
defaultModel:
|
|
29
|
+
defaultModel: DEFAULT_BEST_MODELS.geminiCli,
|
|
21
30
|
freeTier: true,
|
|
22
31
|
note: 'Free tier available. Get a key at https://aistudio.google.com/app/apikey',
|
|
23
32
|
},
|
|
@@ -29,6 +38,22 @@ export const PROVIDERS = {
|
|
|
29
38
|
freeTier: true,
|
|
30
39
|
note: 'Free tier with rate limits. Get a key at https://console.groq.com/keys',
|
|
31
40
|
},
|
|
41
|
+
kimi: {
|
|
42
|
+
name: 'kimi',
|
|
43
|
+
baseURL: 'https://api.moonshot.ai/v1',
|
|
44
|
+
apiKeyEnv: 'KIMI_API_KEY',
|
|
45
|
+
defaultModel: 'kimi-k2.5',
|
|
46
|
+
freeTier: false,
|
|
47
|
+
note: 'Moonshot AI Kimi OpenAI-compatible API. Set KIMI_API_KEY or PATINA_API_KEY.',
|
|
48
|
+
},
|
|
49
|
+
moonshot: {
|
|
50
|
+
name: 'moonshot',
|
|
51
|
+
baseURL: 'https://api.moonshot.ai/v1',
|
|
52
|
+
apiKeyEnv: 'MOONSHOT_API_KEY',
|
|
53
|
+
defaultModel: 'kimi-k2.5',
|
|
54
|
+
freeTier: false,
|
|
55
|
+
note: 'Moonshot AI Kimi OpenAI-compatible API. Set MOONSHOT_API_KEY or PATINA_API_KEY.',
|
|
56
|
+
},
|
|
32
57
|
together: {
|
|
33
58
|
name: 'together',
|
|
34
59
|
baseURL: 'https://api.together.xyz/v1',
|
|
@@ -39,6 +64,15 @@ export const PROVIDERS = {
|
|
|
39
64
|
},
|
|
40
65
|
};
|
|
41
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a provider preset by name.
|
|
69
|
+
*
|
|
70
|
+
* @param {string|null|undefined} name Provider name; falsy returns null.
|
|
71
|
+
* @returns {object|null} Provider preset or null.
|
|
72
|
+
* @throws {PatinaCliError} When name is unknown.
|
|
73
|
+
* @example
|
|
74
|
+
* const provider = selectProvider('openai');
|
|
75
|
+
*/
|
|
42
76
|
export function selectProvider(name) {
|
|
43
77
|
if (!name) return null;
|
|
44
78
|
const provider = PROVIDERS[name];
|
|
@@ -46,12 +80,25 @@ export function selectProvider(name) {
|
|
|
46
80
|
throw inputError(
|
|
47
81
|
`Unknown provider: ${name}`,
|
|
48
82
|
`Available providers are: ${Object.keys(PROVIDERS).join(', ')}.`,
|
|
49
|
-
'Run `patina --
|
|
83
|
+
'Run `patina --help` to see provider presets.'
|
|
50
84
|
);
|
|
51
85
|
}
|
|
52
86
|
return provider;
|
|
53
87
|
}
|
|
54
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Resolve effective API key, base URL, and model from explicit values, provider, and env.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} options Provider resolution inputs.
|
|
93
|
+
* @param {object|null} [options.provider] Provider preset from {@link selectProvider}.
|
|
94
|
+
* @param {string} [options.apiKey] Explicit API key.
|
|
95
|
+
* @param {string} [options.baseURL] Explicit base URL.
|
|
96
|
+
* @param {string} [options.model] Explicit model id.
|
|
97
|
+
* @returns {{apiKey: string|null, baseURL: string, model: string, apiKeySource: string|null, baseURLSource: string|null, modelSource: string|null}} Resolved provider config.
|
|
98
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
99
|
+
* @example
|
|
100
|
+
* const resolved = resolveProviderConfig({ provider: selectProvider('openai') });
|
|
101
|
+
*/
|
|
55
102
|
export function resolveProviderConfig({ provider, apiKey, baseURL, model }) {
|
|
56
103
|
// Explicit args win. Then provider preset. Then PATINA_* env vars.
|
|
57
104
|
// Returns the resolved { apiKey, baseURL, model } and the source for each
|
|
@@ -92,7 +139,7 @@ export function resolveProviderConfig({ provider, apiKey, baseURL, model }) {
|
|
|
92
139
|
resolved.baseURLSource = process.env.PATINA_API_BASE ? 'env:PATINA_API_BASE' : 'default';
|
|
93
140
|
}
|
|
94
141
|
if (!resolved.model) {
|
|
95
|
-
resolved.model = process.env.PATINA_MODEL ||
|
|
142
|
+
resolved.model = process.env.PATINA_MODEL || DEFAULT_BEST_MODELS.openai;
|
|
96
143
|
resolved.modelSource = process.env.PATINA_MODEL ? 'env:PATINA_MODEL' : 'default';
|
|
97
144
|
}
|
|
98
145
|
|
package/src/scoring.js
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import { callLLM as defaultCallLLM } from './api.js';
|
|
2
3
|
import { getRepoRoot } from './config.js';
|
|
3
4
|
import { analyzeText } from './features/index.js';
|
|
5
|
+
import { summarizeSignalStrength } from './features/signal-strength.js';
|
|
4
6
|
import { createLogger } from './logger.js';
|
|
5
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Default maximum delta before deterministic and LLM scores are reconciled upward.
|
|
10
|
+
*
|
|
11
|
+
* @type {number}
|
|
12
|
+
* @example
|
|
13
|
+
* const threshold = DEFAULT_DETERMINISTIC_DIVERGENCE_THRESHOLD;
|
|
14
|
+
*/
|
|
6
15
|
export const DEFAULT_DETERMINISTIC_DIVERGENCE_THRESHOLD = 20;
|
|
7
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Score floor applied when deterministic markup-leakage is detected.
|
|
19
|
+
*
|
|
20
|
+
* Model-output leakage (issue #332) is near-proof-grade: a single token that
|
|
21
|
+
* LLM tooling injects and humans never type. Unlike the stylometric/lexical
|
|
22
|
+
* signals it is decisive on its own, so any hit short-circuits the deterministic
|
|
23
|
+
* `overall` into the 'heavily AI' band (>70) regardless of the per-paragraph
|
|
24
|
+
* hot ratio. It is a floor, not a hard 100, because the surrounding prose may
|
|
25
|
+
* still be genuinely human and we avoid claiming absolute proof.
|
|
26
|
+
*
|
|
27
|
+
* @type {number}
|
|
28
|
+
*/
|
|
29
|
+
export const LEAKAGE_SCORE_FLOOR = 90;
|
|
30
|
+
|
|
8
31
|
class SchemaError extends Error {
|
|
9
32
|
constructor(message, raw) {
|
|
10
33
|
super(message);
|
|
@@ -43,7 +66,8 @@ async function callAndParseJson({
|
|
|
43
66
|
temperature = 0.1,
|
|
44
67
|
deadline,
|
|
45
68
|
signal,
|
|
46
|
-
|
|
69
|
+
timeout,
|
|
70
|
+
callLLM = /** @type {Function} */ (defaultCallLLM),
|
|
47
71
|
logger = createLogger(),
|
|
48
72
|
now,
|
|
49
73
|
sleep,
|
|
@@ -59,6 +83,7 @@ async function callAndParseJson({
|
|
|
59
83
|
temperature: t,
|
|
60
84
|
deadline,
|
|
61
85
|
signal,
|
|
86
|
+
timeout,
|
|
62
87
|
now,
|
|
63
88
|
sleep,
|
|
64
89
|
});
|
|
@@ -76,6 +101,28 @@ async function callAndParseJson({
|
|
|
76
101
|
throw lastError;
|
|
77
102
|
}
|
|
78
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Score text for AI-likeness using an LLM JSON scorer plus deterministic shadow signals.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} options Scoring options.
|
|
108
|
+
* @param {string} options.text Text to score.
|
|
109
|
+
* @param {object} options.config Effective patina config.
|
|
110
|
+
* @param {object[]} options.patterns Loaded pattern packs, retained for scorer compatibility.
|
|
111
|
+
* @param {string} [options.apiKey] Provider API key.
|
|
112
|
+
* @param {string} [options.baseURL] Provider base URL.
|
|
113
|
+
* @param {string} [options.model] Model id.
|
|
114
|
+
* @param {number} [options.deadline] Absolute epoch-millisecond deadline.
|
|
115
|
+
* @param {AbortSignal} [options.signal] External cancellation signal.
|
|
116
|
+
* @param {number} [options.timeout] Per-attempt backend timeout in milliseconds.
|
|
117
|
+
* @param {Function} [options.callLLM] Injectable LLM implementation.
|
|
118
|
+
* @param {object} [options.logger] patina logger.
|
|
119
|
+
* @param {Function} [options.now] Clock returning epoch milliseconds.
|
|
120
|
+
* @param {Function} [options.sleep] Sleep helper for tests.
|
|
121
|
+
* @returns {Promise<object>} Score payload with overall, interpretation, llmScore, and deterministicScore.
|
|
122
|
+
* @throws {Error} When the operation is aborted.
|
|
123
|
+
* @example
|
|
124
|
+
* const score = await scoreText({ text: 'Draft', config, patterns, callLLM: async () => '{"categories":{},"overall":20,"interpretation":"mostly human"}' });
|
|
125
|
+
*/
|
|
79
126
|
export async function scoreText({
|
|
80
127
|
text,
|
|
81
128
|
config,
|
|
@@ -85,6 +132,7 @@ export async function scoreText({
|
|
|
85
132
|
model,
|
|
86
133
|
deadline,
|
|
87
134
|
signal,
|
|
135
|
+
timeout,
|
|
88
136
|
callLLM = defaultCallLLM,
|
|
89
137
|
logger = createLogger(),
|
|
90
138
|
now,
|
|
@@ -133,6 +181,7 @@ ${text}
|
|
|
133
181
|
deadline,
|
|
134
182
|
signal,
|
|
135
183
|
callLLM,
|
|
184
|
+
timeout,
|
|
136
185
|
logger,
|
|
137
186
|
now,
|
|
138
187
|
sleep,
|
|
@@ -153,6 +202,19 @@ ${text}
|
|
|
153
202
|
}
|
|
154
203
|
}
|
|
155
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Compute deterministic stylometry/lexicon AI-likeness signals.
|
|
207
|
+
*
|
|
208
|
+
* @param {object} [options] Deterministic scoring options.
|
|
209
|
+
* @param {string} [options.text] Text to analyze.
|
|
210
|
+
* @param {object} [options.config={}] Effective config.
|
|
211
|
+
* @param {string} [options.repoRoot] Repository root for analyzer resources.
|
|
212
|
+
* @param {Function} [options.analyzer] Analyzer implementation.
|
|
213
|
+
* @returns {object|null} Deterministic score payload, skipped payload, or null when disabled.
|
|
214
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
215
|
+
* @example
|
|
216
|
+
* const deterministic = scoreDeterministicSignals({ text: 'Draft', config });
|
|
217
|
+
*/
|
|
156
218
|
export function scoreDeterministicSignals({
|
|
157
219
|
text,
|
|
158
220
|
config = {},
|
|
@@ -172,23 +234,37 @@ export function scoreDeterministicSignals({
|
|
|
172
234
|
skipReason: 'language-disabled',
|
|
173
235
|
paragraphCount: 0,
|
|
174
236
|
hotParagraphs: 0,
|
|
237
|
+
signalScore: 0,
|
|
175
238
|
bands: emptyDeterministicBands(),
|
|
176
239
|
};
|
|
177
240
|
}
|
|
178
241
|
|
|
179
242
|
try {
|
|
243
|
+
const lexiconAllowed = isLexiconEnabledForLanguage(config, lang);
|
|
180
244
|
const result = analyzer(String(text || ''), {
|
|
181
245
|
lang,
|
|
182
246
|
repoRoot,
|
|
183
247
|
burstinessBands: config.stylometry?.burstiness?.bands,
|
|
184
248
|
mattrBands: config.stylometry?.ttr?.bands,
|
|
185
249
|
mattrWindow: config.stylometry?.ttr?.window,
|
|
250
|
+
koDiagnosticsEnabled: config.stylometry?.ko_diagnostics?.enabled !== false,
|
|
251
|
+
koDiagnosticBands: config.stylometry?.ko_diagnostics?.bands,
|
|
186
252
|
lexiconDensityThreshold: config.lexicon?.density_threshold,
|
|
253
|
+
...(lexiconAllowed ? {} : { lexicon: { lang, path: null, strict: [], phrases: [] } }),
|
|
187
254
|
});
|
|
188
255
|
const paragraphs = Array.isArray(result?.paragraphs) ? result.paragraphs : [];
|
|
189
256
|
const paragraphCount = paragraphs.length;
|
|
190
257
|
const hotParagraphs = paragraphs.filter((p) => p.hot).length;
|
|
191
|
-
const
|
|
258
|
+
const hotRatioOverall = paragraphCount > 0 ? roundScore((hotParagraphs / paragraphCount) * 100) : 0;
|
|
259
|
+
// Model-output leakage (#332) is near-proof-grade and lives at the document
|
|
260
|
+
// level, so it short-circuits the hot-ratio score into the 'heavily AI' band.
|
|
261
|
+
const leaked = Boolean(result?.markupLeakage?.leaked);
|
|
262
|
+
const overall = leaked ? Math.max(hotRatioOverall, LEAKAGE_SCORE_FLOOR) : hotRatioOverall;
|
|
263
|
+
const signalScore = roundScore(summarizeSignalStrength(paragraphs, {
|
|
264
|
+
burstinessBands: config.stylometry?.burstiness?.bands,
|
|
265
|
+
mattrBands: config.stylometry?.ttr?.bands,
|
|
266
|
+
lexiconDensityThreshold: config.lexicon?.density_threshold,
|
|
267
|
+
}));
|
|
192
268
|
|
|
193
269
|
return {
|
|
194
270
|
overall,
|
|
@@ -197,6 +273,7 @@ export function scoreDeterministicSignals({
|
|
|
197
273
|
skipReason: result?.skipReason ?? null,
|
|
198
274
|
paragraphCount,
|
|
199
275
|
hotParagraphs,
|
|
276
|
+
signalScore,
|
|
200
277
|
bands: {
|
|
201
278
|
burstiness: countBands(paragraphs.map((p) => p.burstiness?.band)),
|
|
202
279
|
mattr: countBands(paragraphs.map((p) => p.mattr?.band)),
|
|
@@ -204,6 +281,15 @@ export function scoreDeterministicSignals({
|
|
|
204
281
|
hot: paragraphs.filter((p) => p.lexicon?.hot).length,
|
|
205
282
|
threshold: config.lexicon?.density_threshold ?? null,
|
|
206
283
|
},
|
|
284
|
+
koDiagnostics: {
|
|
285
|
+
hot: paragraphs.filter((p) => p.koDiagnostics?.hot).length,
|
|
286
|
+
thresholds: config.stylometry?.ko_diagnostics?.bands ?? null,
|
|
287
|
+
},
|
|
288
|
+
markupLeakage: {
|
|
289
|
+
leaked,
|
|
290
|
+
hits: Array.isArray(result?.markupLeakage?.hits) ? result.markupLeakage.hits.length : 0,
|
|
291
|
+
floor: LEAKAGE_SCORE_FLOOR,
|
|
292
|
+
},
|
|
207
293
|
},
|
|
208
294
|
};
|
|
209
295
|
} catch (err) {
|
|
@@ -214,12 +300,26 @@ export function scoreDeterministicSignals({
|
|
|
214
300
|
skipReason: 'deterministic-failure',
|
|
215
301
|
paragraphCount: 0,
|
|
216
302
|
hotParagraphs: 0,
|
|
303
|
+
signalScore: 0,
|
|
217
304
|
bands: emptyDeterministicBands(),
|
|
218
305
|
error: err?.message || 'deterministic scoring failed',
|
|
219
306
|
};
|
|
220
307
|
}
|
|
221
308
|
}
|
|
222
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Merge an LLM score payload with deterministic shadow-score reconciliation.
|
|
312
|
+
*
|
|
313
|
+
* @param {object} parsed Parsed LLM scoring JSON.
|
|
314
|
+
* @param {object} [options] Reconciliation options.
|
|
315
|
+
* @param {object|null} [options.deterministicScore] Deterministic score payload.
|
|
316
|
+
* @param {object} [options.config={}] Effective config.
|
|
317
|
+
* @param {object} [options.logger] Logger for reconciliation warnings.
|
|
318
|
+
* @returns {object} Score payload preserving llmScore and deterministicScore details.
|
|
319
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
320
|
+
* @example
|
|
321
|
+
* const score = withShadowScore({ overall: 20 }, { deterministicScore: { overall: 25 } });
|
|
322
|
+
*/
|
|
223
323
|
export function withShadowScore(parsed, { deterministicScore, config = {}, logger } = {}) {
|
|
224
324
|
const llmOverall = toFiniteScore(parsed?.overall);
|
|
225
325
|
const llmScore = {
|
|
@@ -246,6 +346,19 @@ export function withShadowScore(parsed, { deterministicScore, config = {}, logge
|
|
|
246
346
|
};
|
|
247
347
|
}
|
|
248
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Reconcile LLM and deterministic overall scores according to config thresholds.
|
|
351
|
+
*
|
|
352
|
+
* @param {object} [options] Reconciliation inputs.
|
|
353
|
+
* @param {number|null} [options.llmOverall] LLM overall score.
|
|
354
|
+
* @param {object|null} [options.deterministicScore] Deterministic score payload.
|
|
355
|
+
* @param {object} [options.config={}] Effective config.
|
|
356
|
+
* @param {object} [options.logger] Logger for warnings.
|
|
357
|
+
* @returns {{overall: number|null, scorePreference: (object|null)}} Reconciled score and preference source.
|
|
358
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
359
|
+
* @example
|
|
360
|
+
* const result = reconcileScoreOverall({ llmOverall: 20, deterministicScore: { overall: 60 } });
|
|
361
|
+
*/
|
|
249
362
|
export function reconcileScoreOverall({
|
|
250
363
|
llmOverall,
|
|
251
364
|
deterministicScore,
|
|
@@ -281,6 +394,27 @@ export function reconcileScoreOverall({
|
|
|
281
394
|
return { overall, scorePreference };
|
|
282
395
|
}
|
|
283
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Score meaning preservation between original and rewritten text.
|
|
399
|
+
*
|
|
400
|
+
* @param {object} options MPS options.
|
|
401
|
+
* @param {string} options.original Original text.
|
|
402
|
+
* @param {string} options.rewritten Rewritten text.
|
|
403
|
+
* @param {string} [options.apiKey] Provider API key.
|
|
404
|
+
* @param {string} [options.baseURL] Provider base URL.
|
|
405
|
+
* @param {string} [options.model] Model id.
|
|
406
|
+
* @param {number} [options.deadline] Absolute epoch-millisecond deadline.
|
|
407
|
+
* @param {AbortSignal} [options.signal] External cancellation signal.
|
|
408
|
+
* @param {number} [options.timeout] Per-attempt backend timeout in milliseconds.
|
|
409
|
+
* @param {Function} [options.callLLM] Injectable LLM implementation.
|
|
410
|
+
* @param {object} [options.logger] patina logger.
|
|
411
|
+
* @param {Function} [options.now] Clock returning epoch milliseconds.
|
|
412
|
+
* @param {Function} [options.sleep] Sleep helper for tests.
|
|
413
|
+
* @returns {Promise<Object>} MPS result.
|
|
414
|
+
* @throws {Error} When the operation is aborted.
|
|
415
|
+
* @example
|
|
416
|
+
* const mps = await scoreMPS({ original: 'A', rewritten: 'A', callLLM: async () => '{"mps":100,"anchors":[]}' });
|
|
417
|
+
*/
|
|
284
418
|
export async function scoreMPS({
|
|
285
419
|
original,
|
|
286
420
|
rewritten,
|
|
@@ -289,6 +423,7 @@ export async function scoreMPS({
|
|
|
289
423
|
model,
|
|
290
424
|
deadline,
|
|
291
425
|
signal,
|
|
426
|
+
timeout,
|
|
292
427
|
callLLM = defaultCallLLM,
|
|
293
428
|
logger = createLogger(),
|
|
294
429
|
now,
|
|
@@ -333,6 +468,7 @@ ${rewritten}
|
|
|
333
468
|
model,
|
|
334
469
|
deadline,
|
|
335
470
|
signal,
|
|
471
|
+
timeout,
|
|
336
472
|
callLLM,
|
|
337
473
|
logger,
|
|
338
474
|
now,
|
|
@@ -348,6 +484,15 @@ ${rewritten}
|
|
|
348
484
|
}
|
|
349
485
|
}
|
|
350
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Convert a numeric AI-likeness score to a human-readable band.
|
|
489
|
+
*
|
|
490
|
+
* @param {number} score AI-likeness score from 0 to 100.
|
|
491
|
+
* @returns {string} Interpretation band.
|
|
492
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
493
|
+
* @example
|
|
494
|
+
* const label = interpretScore(28); // mostly human
|
|
495
|
+
*/
|
|
351
496
|
export function interpretScore(score) {
|
|
352
497
|
if (score <= 15) return 'human';
|
|
353
498
|
if (score <= 30) return 'mostly human';
|
|
@@ -357,6 +502,16 @@ export function interpretScore(score) {
|
|
|
357
502
|
}
|
|
358
503
|
|
|
359
504
|
// Length ratio is deterministic — bucket per core/scoring.md §10.4.
|
|
505
|
+
/**
|
|
506
|
+
* Score rewritten length ratio on the 0-3 fidelity scale.
|
|
507
|
+
*
|
|
508
|
+
* @param {string} original Original text.
|
|
509
|
+
* @param {string} rewritten Rewritten text.
|
|
510
|
+
* @returns {number} Length-ratio points from 0 to 3.
|
|
511
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
512
|
+
* @example
|
|
513
|
+
* const points = lengthRatioPoints('abcd', 'abcde');
|
|
514
|
+
*/
|
|
360
515
|
export function lengthRatioPoints(original, rewritten) {
|
|
361
516
|
if (!original || original.length === 0) return 3;
|
|
362
517
|
const ratio = (rewritten.length / original.length) * 100;
|
|
@@ -366,6 +521,27 @@ export function lengthRatioPoints(original, rewritten) {
|
|
|
366
521
|
return 0;
|
|
367
522
|
}
|
|
368
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Score fidelity between original and rewritten text using length plus LLM criteria.
|
|
526
|
+
*
|
|
527
|
+
* @param {object} options Fidelity options.
|
|
528
|
+
* @param {string} options.original Original text.
|
|
529
|
+
* @param {string} options.rewritten Rewritten text.
|
|
530
|
+
* @param {string} [options.apiKey] Provider API key.
|
|
531
|
+
* @param {string} [options.baseURL] Provider base URL.
|
|
532
|
+
* @param {string} [options.model] Model id.
|
|
533
|
+
* @param {number} [options.deadline] Absolute epoch-millisecond deadline.
|
|
534
|
+
* @param {AbortSignal} [options.signal] External cancellation signal.
|
|
535
|
+
* @param {number} [options.timeout] Per-attempt backend timeout in milliseconds.
|
|
536
|
+
* @param {Function} [options.callLLM] Injectable LLM implementation.
|
|
537
|
+
* @param {object} [options.logger] patina logger.
|
|
538
|
+
* @param {Function} [options.now] Clock returning epoch milliseconds.
|
|
539
|
+
* @param {Function} [options.sleep] Sleep helper for tests.
|
|
540
|
+
* @returns {Promise<Object>} Fidelity result.
|
|
541
|
+
* @throws {Error} When the operation is aborted.
|
|
542
|
+
* @example
|
|
543
|
+
* const fidelity = await scoreFidelity({ original: 'A', rewritten: 'A', callLLM: async () => '{"criteria":{"meaning":3,"tone":3,"no_unintended_additions":3}}' });
|
|
544
|
+
*/
|
|
369
545
|
export async function scoreFidelity({
|
|
370
546
|
original,
|
|
371
547
|
rewritten,
|
|
@@ -374,6 +550,7 @@ export async function scoreFidelity({
|
|
|
374
550
|
model,
|
|
375
551
|
deadline,
|
|
376
552
|
signal,
|
|
553
|
+
timeout,
|
|
377
554
|
callLLM = defaultCallLLM,
|
|
378
555
|
logger = createLogger(),
|
|
379
556
|
now,
|
|
@@ -421,6 +598,7 @@ ${rewritten}
|
|
|
421
598
|
deadline,
|
|
422
599
|
signal,
|
|
423
600
|
callLLM,
|
|
601
|
+
timeout,
|
|
424
602
|
logger,
|
|
425
603
|
now,
|
|
426
604
|
sleep,
|
|
@@ -453,6 +631,15 @@ ${rewritten}
|
|
|
453
631
|
};
|
|
454
632
|
}
|
|
455
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Clamp and round a value into the inclusive 0-3 scoring range.
|
|
636
|
+
*
|
|
637
|
+
* @param {number|string} v Value to clamp.
|
|
638
|
+
* @returns {number} Integer from 0 to 3.
|
|
639
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
640
|
+
* @example
|
|
641
|
+
* const value = clamp03(4.2); // 3
|
|
642
|
+
*/
|
|
456
643
|
export function clamp03(v) {
|
|
457
644
|
const n = Number(v);
|
|
458
645
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -467,6 +654,20 @@ function rethrowIfAborted(err, signal) {
|
|
|
467
654
|
|
|
468
655
|
// Combined score per core/scoring.md §13: AI-likeness × ai_weight + (100 - fidelity) × fidelity_weight.
|
|
469
656
|
// Lower is better. Falls back to default weights if profile not configured.
|
|
657
|
+
/**
|
|
658
|
+
* Combine AI-likeness, inverted fidelity, and optional deterministic score.
|
|
659
|
+
*
|
|
660
|
+
* @param {object} options Combined score inputs.
|
|
661
|
+
* @param {number} options.aiLikeness AI-likeness score, lower is better.
|
|
662
|
+
* @param {number} options.fidelity Fidelity score, higher is better.
|
|
663
|
+
* @param {string} [options.profile] Profile name for configured weights.
|
|
664
|
+
* @param {object} [options.config] Effective config.
|
|
665
|
+
* @param {number|object|null} [options.deterministicScore] Optional deterministic score.
|
|
666
|
+
* @returns {number} Combined score, lower is better.
|
|
667
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
668
|
+
* @example
|
|
669
|
+
* const score = combinedScore({ aiLikeness: 20, fidelity: 90, profile: 'default', config: {} });
|
|
670
|
+
*/
|
|
470
671
|
export function combinedScore({ aiLikeness, fidelity, profile, config, deterministicScore }) {
|
|
471
672
|
const profileWeights = config?.ouroboros?.['combined-weights']?.[profile];
|
|
472
673
|
const ai = profileWeights?.['ai-likeness'] ?? 0.6;
|
|
@@ -484,6 +685,12 @@ export function combinedScore({ aiLikeness, fidelity, profile, config, determini
|
|
|
484
685
|
return roundScore(aiLikeness * ai + fidelityInverted * fid);
|
|
485
686
|
}
|
|
486
687
|
|
|
688
|
+
function isLexiconEnabledForLanguage(config = {}, lang) {
|
|
689
|
+
if (config.lexicon?.enabled === false) return false;
|
|
690
|
+
const enabledLanguages = config.lexicon?.languages;
|
|
691
|
+
return !Array.isArray(enabledLanguages) || enabledLanguages.includes(lang);
|
|
692
|
+
}
|
|
693
|
+
|
|
487
694
|
function deterministicScoringOptions(config = {}) {
|
|
488
695
|
const cfg = config.scoring?.deterministic || {};
|
|
489
696
|
const enabled = cfg.enabled !== false;
|
|
@@ -517,6 +724,7 @@ function emptyDeterministicBands() {
|
|
|
517
724
|
burstiness: { low: 0, mid: 0, high: 0, null: 0 },
|
|
518
725
|
mattr: { low: 0, mid: 0, high: 0, null: 0 },
|
|
519
726
|
lexicon: { hot: 0, threshold: null },
|
|
727
|
+
koDiagnostics: { hot: 0, thresholds: null },
|
|
520
728
|
};
|
|
521
729
|
}
|
|
522
730
|
|
package/src/security.js
CHANGED
|
@@ -8,6 +8,15 @@ import { inputError } from './errors.js';
|
|
|
8
8
|
|
|
9
9
|
const PROFILE_NAME_RE = /^[A-Za-z0-9_][A-Za-z0-9_-]*$/;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Validate a profile name before resolving profiles/{name}.md.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} name Profile name supplied by CLI or config.
|
|
15
|
+
* @returns {void}
|
|
16
|
+
* @throws {PatinaCliError} When the name is empty, non-string, or contains unsafe characters.
|
|
17
|
+
* @example
|
|
18
|
+
* validateProfileName('default');
|
|
19
|
+
*/
|
|
11
20
|
export function validateProfileName(name) {
|
|
12
21
|
if (typeof name !== 'string' || !PROFILE_NAME_RE.test(name)) {
|
|
13
22
|
throw inputError(
|
|
@@ -18,6 +27,15 @@ export function validateProfileName(name) {
|
|
|
18
27
|
}
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Check whether a hostname is localhost or loopback.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} hostname Hostname from a URL.
|
|
34
|
+
* @returns {boolean} True for localhost, 127/8, or ::1.
|
|
35
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
36
|
+
* @example
|
|
37
|
+
* const local = isLoopbackHost('127.0.0.1');
|
|
38
|
+
*/
|
|
21
39
|
export function isLoopbackHost(hostname) {
|
|
22
40
|
if (!hostname) return false;
|
|
23
41
|
if (hostname === 'localhost') return true;
|
|
@@ -31,6 +49,15 @@ export function isLoopbackHost(hostname) {
|
|
|
31
49
|
// so DNS rebinding is NOT covered by this check. The goal is to catch the
|
|
32
50
|
// common case: --base-url pointed at 169.254.169.254 (cloud metadata) or
|
|
33
51
|
// internal RFC 1918 hosts that should not receive Bearer tokens.
|
|
52
|
+
/**
|
|
53
|
+
* Detect literal private, reserved, link-local, metadata, or multicast IP hosts.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} hostname Hostname or bracketed IPv6 literal.
|
|
56
|
+
* @returns {boolean} True when the literal IP is private or special-use.
|
|
57
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
58
|
+
* @example
|
|
59
|
+
* const blocked = isPrivateOrSpecialIP('169.254.169.254');
|
|
60
|
+
*/
|
|
34
61
|
export function isPrivateOrSpecialIP(hostname) {
|
|
35
62
|
if (!hostname) return false;
|
|
36
63
|
const h = hostname.startsWith('[') && hostname.endsWith(']')
|
|
@@ -63,6 +90,18 @@ export function isPrivateOrSpecialIP(hostname) {
|
|
|
63
90
|
return false;
|
|
64
91
|
}
|
|
65
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Validate a provider base URL before sending prompts and bearer tokens.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} baseURL URL to validate.
|
|
97
|
+
* @param {object} [options] Validation opt-ins.
|
|
98
|
+
* @param {boolean} [options.allowInsecure=false] Allow non-loopback HTTP.
|
|
99
|
+
* @param {boolean} [options.allowPrivate=false] Allow private/reserved literal IPs.
|
|
100
|
+
* @returns {void}
|
|
101
|
+
* @throws {PatinaCliError} When the URL is invalid, unsupported, insecure, or private without opt-in.
|
|
102
|
+
* @example
|
|
103
|
+
* validateBaseURL('https://api.openai.com/v1');
|
|
104
|
+
*/
|
|
66
105
|
export function validateBaseURL(baseURL, { allowInsecure = false, allowPrivate = false } = {}) {
|
|
67
106
|
let url;
|
|
68
107
|
try {
|
|
@@ -108,24 +147,60 @@ export function validateBaseURL(baseURL, { allowInsecure = false, allowPrivate =
|
|
|
108
147
|
}
|
|
109
148
|
}
|
|
110
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Read CLI/env opt-in for non-loopback HTTP base URLs.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} [parsed] Parsed CLI options.
|
|
154
|
+
* @returns {boolean} True when insecure base URLs are explicitly allowed.
|
|
155
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
156
|
+
* @example
|
|
157
|
+
* const allowed = shouldAllowInsecureBaseURL({ allowInsecureBaseURL: true });
|
|
158
|
+
*/
|
|
111
159
|
export function shouldAllowInsecureBaseURL(parsed) {
|
|
112
160
|
if (parsed && parsed.allowInsecureBaseURL) return true;
|
|
113
161
|
const env = process.env.PATINA_ALLOW_INSECURE_BASE_URL;
|
|
114
162
|
return env === '1' || env === 'true' || env === 'yes';
|
|
115
163
|
}
|
|
116
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Persist CLI insecure-base-url opt-in into process.env for downstream calls.
|
|
167
|
+
*
|
|
168
|
+
* @param {object} [parsed] Parsed CLI options.
|
|
169
|
+
* @returns {void}
|
|
170
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
171
|
+
* @example
|
|
172
|
+
* applyInsecureBaseURLOptIn({ allowInsecureBaseURL: true });
|
|
173
|
+
*/
|
|
117
174
|
export function applyInsecureBaseURLOptIn(parsed) {
|
|
118
175
|
if (parsed && parsed.allowInsecureBaseURL) {
|
|
119
176
|
process.env.PATINA_ALLOW_INSECURE_BASE_URL = '1';
|
|
120
177
|
}
|
|
121
178
|
}
|
|
122
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Read CLI/env opt-in for private or reserved literal IP base URLs.
|
|
182
|
+
*
|
|
183
|
+
* @param {object} [parsed] Parsed CLI options.
|
|
184
|
+
* @returns {boolean} True when private base URLs are explicitly allowed.
|
|
185
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
186
|
+
* @example
|
|
187
|
+
* const allowed = shouldAllowPrivateBaseURL({ allowPrivateBaseURL: true });
|
|
188
|
+
*/
|
|
123
189
|
export function shouldAllowPrivateBaseURL(parsed) {
|
|
124
190
|
if (parsed && parsed.allowPrivateBaseURL) return true;
|
|
125
191
|
const env = process.env.PATINA_ALLOW_PRIVATE_BASE_URL;
|
|
126
192
|
return env === '1' || env === 'true' || env === 'yes';
|
|
127
193
|
}
|
|
128
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Persist CLI private-base-url opt-in into process.env for downstream calls.
|
|
197
|
+
*
|
|
198
|
+
* @param {object} [parsed] Parsed CLI options.
|
|
199
|
+
* @returns {void}
|
|
200
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
201
|
+
* @example
|
|
202
|
+
* applyPrivateBaseURLOptIn({ allowPrivateBaseURL: true });
|
|
203
|
+
*/
|
|
129
204
|
export function applyPrivateBaseURLOptIn(parsed) {
|
|
130
205
|
if (parsed && parsed.allowPrivateBaseURL) {
|
|
131
206
|
process.env.PATINA_ALLOW_PRIVATE_BASE_URL = '1';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
fixture_id: en-public-docs-01
|
|
3
|
+
language: en
|
|
4
|
+
profile: default
|
|
5
|
+
register: public-docs
|
|
6
|
+
source_type: synthetic-ai
|
|
7
|
+
model_family: fixture
|
|
8
|
+
prompt_id: live-quality-v2
|
|
9
|
+
redistribution: repo-ok
|
|
10
|
+
anchors:
|
|
11
|
+
- coffee
|
|
12
|
+
- Paris
|
|
13
|
+
- Tokyo
|
|
14
|
+
- climate change
|
|
15
|
+
- supply chains
|
|
16
|
+
expected_focus:
|
|
17
|
+
- reduce promotional abstractions
|
|
18
|
+
- preserve concrete locations and constraints
|
|
19
|
+
---
|
|
20
|
+
Coffee has emerged as a pivotal cultural phenomenon that has fundamentally transformed social interactions across the globe. This beloved beverage serves as a catalyst for community building, fosters meaningful connections, and facilitates cross-cultural dialogue.
|
|
21
|
+
|
|
22
|
+
In Paris, some cafes still work as neighborhood meeting places. In Tokyo, compact coffee bars often serve commuters who stay for only a few minutes.
|
|
23
|
+
|
|
24
|
+
Roasters are also dealing with climate change. Heat, irregular rain, and disease pressure have made harvest volume less predictable in several growing regions.
|
|
25
|
+
|
|
26
|
+
Supply chains are part of the same problem. A delayed shipment can change a small cafe's menu for a week, even when demand stays steady.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
fixture_id: ko-public-docs-01
|
|
3
|
+
language: ko
|
|
4
|
+
profile: default
|
|
5
|
+
register: public-docs
|
|
6
|
+
source_type: synthetic-ai
|
|
7
|
+
model_family: fixture
|
|
8
|
+
prompt_id: live-quality-v2
|
|
9
|
+
redistribution: repo-ok
|
|
10
|
+
anchors:
|
|
11
|
+
- 커피
|
|
12
|
+
- 서울
|
|
13
|
+
- 부산
|
|
14
|
+
- 기후 변화
|
|
15
|
+
- 공급망
|
|
16
|
+
expected_focus:
|
|
17
|
+
- 번역투 완화
|
|
18
|
+
- 장소와 제약 보존
|
|
19
|
+
---
|
|
20
|
+
커피는 현대 사회적 상호작용을 근본적으로 변화시킨 핵심적인 문화 현상으로 자리매김하고 있습니다. 이 음료는 공동체 형성을 촉진하고 의미 있는 연결을 가능하게 하며, 다양한 문화권 사이의 대화를 활성화하는 중요한 매개체로 기능합니다.
|
|
21
|
+
|
|
22
|
+
서울의 카페 거리는 출근 전 짧게 들르는 사람과 오래 앉아 일하는 사람이 같이 쓰는 공간이다. 부산의 로스터리들은 관광객보다 동네 단골을 먼저 상대하는 곳도 많다.
|
|
23
|
+
|
|
24
|
+
기후 변화는 원두 수급을 흔들고 있다. 산지의 비와 더위가 달라지면 같은 농장의 생두도 해마다 품질 차이가 커진다.
|
|
25
|
+
|
|
26
|
+
공급망 문제도 작지 않다. 선적이 늦어지면 작은 카페는 일주일 메뉴를 바꿔야 하고, 손님 수요와는 별개로 원가가 흔들린다.
|