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/ouroboros.js
CHANGED
|
@@ -3,6 +3,31 @@ import { scoreText, scoreMPS, scoreFidelity, combinedScore } from './scoring.js'
|
|
|
3
3
|
import { buildPrompt } from './prompt-builder.js';
|
|
4
4
|
import { createLogger } from './logger.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Run the iterative Ouroboros rewrite-and-score loop.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} options Ouroboros options.
|
|
10
|
+
* @param {object} options.config Effective config with ouroboros settings.
|
|
11
|
+
* @param {object[]} options.patterns Loaded pattern packs.
|
|
12
|
+
* @param {object|null} options.profile Parsed profile.
|
|
13
|
+
* @param {object|null} options.voice Parsed voice guide.
|
|
14
|
+
* @param {object|null} [options.voiceSample] Optional voice sample payload.
|
|
15
|
+
* @param {object|null} options.scoring Parsed scoring guide.
|
|
16
|
+
* @param {string} options.text Source text to improve.
|
|
17
|
+
* @param {string} [options.apiKey] Provider API key.
|
|
18
|
+
* @param {string} [options.baseURL] Provider base URL.
|
|
19
|
+
* @param {string} [options.model] Model id.
|
|
20
|
+
* @param {Function} [options.callLLM] LLM implementation.
|
|
21
|
+
* @param {Function} [options.now] Clock returning epoch milliseconds.
|
|
22
|
+
* @param {Function} [options.sleep] Sleep helper for tests.
|
|
23
|
+
* @param {AbortSignal} [options.signal] External cancellation signal.
|
|
24
|
+
* @param {number} [options.timeout] Per-attempt backend timeout in milliseconds.
|
|
25
|
+
* @param {object} [options.logger] patina logger.
|
|
26
|
+
* @returns {Promise<{finalText: string, finalScore: number, iterations: number, reason: string, log: object[]}>} Final text and iteration log.
|
|
27
|
+
* @throws {Error} When model calls or scoring fail outside handled schema fallbacks.
|
|
28
|
+
* @example
|
|
29
|
+
* const result = await runOuroboros({ config, patterns, profile, voice, scoring, text });
|
|
30
|
+
*/
|
|
6
31
|
export async function runOuroboros({
|
|
7
32
|
config,
|
|
8
33
|
patterns,
|
|
@@ -18,6 +43,7 @@ export async function runOuroboros({
|
|
|
18
43
|
now,
|
|
19
44
|
sleep,
|
|
20
45
|
signal,
|
|
46
|
+
timeout,
|
|
21
47
|
logger = createLogger(),
|
|
22
48
|
}) {
|
|
23
49
|
const ouroborosConfig = config.ouroboros || {};
|
|
@@ -40,6 +66,7 @@ export async function runOuroboros({
|
|
|
40
66
|
now,
|
|
41
67
|
sleep,
|
|
42
68
|
signal,
|
|
69
|
+
timeout,
|
|
43
70
|
logger,
|
|
44
71
|
});
|
|
45
72
|
|
|
@@ -98,6 +125,7 @@ export async function runOuroboros({
|
|
|
98
125
|
now,
|
|
99
126
|
sleep,
|
|
100
127
|
signal,
|
|
128
|
+
timeout,
|
|
101
129
|
});
|
|
102
130
|
|
|
103
131
|
const scoreResult = await scoreText({
|
|
@@ -111,6 +139,7 @@ export async function runOuroboros({
|
|
|
111
139
|
now,
|
|
112
140
|
sleep,
|
|
113
141
|
signal,
|
|
142
|
+
timeout,
|
|
114
143
|
logger,
|
|
115
144
|
});
|
|
116
145
|
|
|
@@ -134,6 +163,7 @@ export async function runOuroboros({
|
|
|
134
163
|
now,
|
|
135
164
|
sleep,
|
|
136
165
|
signal,
|
|
166
|
+
timeout,
|
|
137
167
|
logger,
|
|
138
168
|
}),
|
|
139
169
|
scoreFidelity({
|
|
@@ -146,6 +176,7 @@ export async function runOuroboros({
|
|
|
146
176
|
now,
|
|
147
177
|
sleep,
|
|
148
178
|
signal,
|
|
179
|
+
timeout,
|
|
149
180
|
logger,
|
|
150
181
|
}),
|
|
151
182
|
]);
|
package/src/output.js
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import { createLogger } from './logger.js';
|
|
2
|
-
|
|
3
|
+
import { analyzeText } from './features/index.js';
|
|
4
|
+
import { TRANSLATIONESE_RULES } from './features/translationese.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format a raw backend result for CLI output mode and requested format.
|
|
8
|
+
*
|
|
9
|
+
* @param {string|object} result Backend result or structured mode result.
|
|
10
|
+
* @param {string} mode Output mode: rewrite, diff, audit, score, or ouroboros.
|
|
11
|
+
* @param {object} [parsed={}] Parsed CLI options.
|
|
12
|
+
* @param {object} [opts={}] Formatting options.
|
|
13
|
+
* @param {object|null} [opts.tone] Tone metadata to append.
|
|
14
|
+
* @param {object} [opts.logger] Logger for output warnings.
|
|
15
|
+
* @param {object} [opts.env] Environment map for color decisions.
|
|
16
|
+
* @param {object} [opts.stdout] Stdout-like stream for color decisions.
|
|
17
|
+
* @param {string} [opts.auditBackstop] Deterministic audit-mode section to append before the tone footer.
|
|
18
|
+
* @returns {string} User-facing formatted output.
|
|
19
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
20
|
+
* @example
|
|
21
|
+
* const output = formatOutput('[BODY]Hi[/BODY]', 'rewrite');
|
|
22
|
+
*/
|
|
3
23
|
export function formatOutput(result, mode, parsed = {}, opts = {}) {
|
|
4
24
|
const tone = opts.tone || null;
|
|
5
25
|
const format = parsed.format || 'markdown';
|
|
6
|
-
|
|
26
|
+
let body = renderFormattedBody(result, mode, parsed, opts);
|
|
27
|
+
|
|
28
|
+
if (mode === 'audit' && format !== 'json' && opts.auditBackstop) {
|
|
29
|
+
body += opts.auditBackstop;
|
|
30
|
+
}
|
|
7
31
|
|
|
8
32
|
if (format === 'json') {
|
|
9
33
|
return formatJsonOutput({ result, mode, body, tone, gate: parsed.gate });
|
|
@@ -18,11 +42,10 @@ export function formatOutput(result, mode, parsed = {}, opts = {}) {
|
|
|
18
42
|
|
|
19
43
|
function renderFormattedBody(result, mode, parsed = {}, opts = {}) {
|
|
20
44
|
let body = renderBody(result);
|
|
21
|
-
// Only rewrite and ouroboros emit [BODY]
|
|
45
|
+
// Only rewrite and ouroboros emit [BODY] tags; diff/audit/score
|
|
22
46
|
// emit tables and don't need the extraction step.
|
|
23
47
|
if (mode === 'rewrite' || mode === 'ouroboros') {
|
|
24
|
-
|
|
25
|
-
body = variants.length > 0 ? formatVariants(variants, body) : stripSelfAudit(body, { logger: opts.logger });
|
|
48
|
+
body = stripSelfAudit(body, { logger: opts.logger });
|
|
26
49
|
}
|
|
27
50
|
if (mode === 'diff') {
|
|
28
51
|
body = colorizeDiff(body, { parsed, env: opts.env, stdout: opts.stdout });
|
|
@@ -54,39 +77,18 @@ function colorizeDiff(body, { parsed = {}, env = process.env, stdout = process.s
|
|
|
54
77
|
}).join('\n');
|
|
55
78
|
}
|
|
56
79
|
|
|
80
|
+
/**
|
|
81
|
+
* @param {object} [options]
|
|
82
|
+
* @param {object} [options.parsed]
|
|
83
|
+
* @param {boolean} [options.parsed.noColor]
|
|
84
|
+
* @param {Record<string, string|undefined>} [options.env]
|
|
85
|
+
* @param {object} [options.stdout]
|
|
86
|
+
* @param {boolean} [options.stdout.isTTY]
|
|
87
|
+
*/
|
|
57
88
|
function shouldColorDiff({ parsed = {}, env = process.env, stdout = process.stdout } = {}) {
|
|
58
89
|
return !parsed.noColor && env.NO_COLOR === undefined && stdout?.isTTY === true;
|
|
59
90
|
}
|
|
60
91
|
|
|
61
|
-
// v3.11 Phase 3.1: extract [VARIANT n]...[/VARIANT] blocks from a model
|
|
62
|
-
// response. Returns an array of { id, text } sorted by id, empty if no
|
|
63
|
-
// variant tags are present.
|
|
64
|
-
export function extractVariants(body) {
|
|
65
|
-
if (!body) return [];
|
|
66
|
-
const re = /\[VARIANT\s*(\d+)\]\s*\n([\s\S]*?)\n\s*\[\/VARIANT\]/g;
|
|
67
|
-
const out = [];
|
|
68
|
-
let m;
|
|
69
|
-
while ((m = re.exec(body)) !== null) {
|
|
70
|
-
const id = parseInt(m[1], 10);
|
|
71
|
-
const text = m[2].trim();
|
|
72
|
-
if (text) out.push({ id, text });
|
|
73
|
-
}
|
|
74
|
-
out.sort((a, b) => a.id - b.id);
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function formatVariants(variants, raw) {
|
|
79
|
-
// Surface each variant with a labeled header so users can copy whichever
|
|
80
|
-
// voice they want. Strip [SELF_AUDIT] and any tail metadata that follows
|
|
81
|
-
// the last [/VARIANT] block, but preserve the YAML footer if present.
|
|
82
|
-
const lastClose = raw.lastIndexOf('[/VARIANT]');
|
|
83
|
-
const tail = lastClose >= 0
|
|
84
|
-
? raw.slice(lastClose + '[/VARIANT]'.length).replace(/\[SELF_AUDIT\][\s\S]*?\[\/SELF_AUDIT\]/g, '').trim()
|
|
85
|
-
: '';
|
|
86
|
-
const blocks = variants.map(({ id, text }) => `## Variant ${id}\n\n${text}`);
|
|
87
|
-
const merged = blocks.join('\n\n');
|
|
88
|
-
return tail ? `${merged}\n\n${tail}` : merged;
|
|
89
|
-
}
|
|
90
92
|
|
|
91
93
|
// v3.11 Phase 1.3: parse the model's score table and check that the Weight
|
|
92
94
|
// column matches the config-supplied category-weights. case-02 found that
|
|
@@ -95,6 +97,16 @@ function formatVariants(variants, raw) {
|
|
|
95
97
|
//
|
|
96
98
|
// Returns an array of human-readable warning strings (empty if everything
|
|
97
99
|
// matches). Caller is responsible for emitting to stderr.
|
|
100
|
+
/**
|
|
101
|
+
* Validate that a model-emitted score table used configured category weights.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} output Score-mode markdown output.
|
|
104
|
+
* @param {object} configWeights Expected category weight map.
|
|
105
|
+
* @returns {string[]} Human-readable warnings for missing, mismatched, or unexpected categories.
|
|
106
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
107
|
+
* @example
|
|
108
|
+
* const warnings = validateScoreWeights('| content | 0.4 | 1 | 10 | 4 |', { content: 0.4 });
|
|
109
|
+
*/
|
|
98
110
|
export function validateScoreWeights(output, configWeights) {
|
|
99
111
|
if (!output || !configWeights || Object.keys(configWeights).length === 0) {
|
|
100
112
|
return [];
|
|
@@ -201,6 +213,17 @@ function normalizeCategoryName(raw) {
|
|
|
201
213
|
// We extract the body block and drop the audit so callers get clean text.
|
|
202
214
|
// If the model didn't honor the tags (older runs, mocked tests, etc.), we
|
|
203
215
|
// fall back to returning the full output untouched.
|
|
216
|
+
/**
|
|
217
|
+
* Remove SELF_AUDIT blocks and unwrap the BODY block from rewrite output.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} body Raw model response.
|
|
220
|
+
* @param {object} [options] Strip options.
|
|
221
|
+
* @param {object} [options.logger] Logger for malformed output warnings.
|
|
222
|
+
* @returns {string} Clean user-facing body text.
|
|
223
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
224
|
+
* @example
|
|
225
|
+
* const clean = stripSelfAudit('[BODY]Hello[/BODY]\n[SELF_AUDIT]ok[/SELF_AUDIT]');
|
|
226
|
+
*/
|
|
204
227
|
export function stripSelfAudit(body, { logger = createLogger() } = {}) {
|
|
205
228
|
if (!body) return body;
|
|
206
229
|
const bodyOpen = body.indexOf('[BODY]');
|
|
@@ -209,7 +232,7 @@ export function stripSelfAudit(body, { logger = createLogger() } = {}) {
|
|
|
209
232
|
const stripped = removeSelfAuditBlocks(body).trim();
|
|
210
233
|
if (stripped !== body.trim()) {
|
|
211
234
|
logger.warn('output.missing_body_tags', {
|
|
212
|
-
message: `[patina] warning: model output omitted [BODY] tags (${body.length} chars); stripped [SELF_AUDIT].
|
|
235
|
+
message: `[patina] warning: model output omitted [BODY] tags (${body.length} chars); stripped [SELF_AUDIT]. Try a different backend if the output looks wrong.`,
|
|
213
236
|
});
|
|
214
237
|
return stripped;
|
|
215
238
|
}
|
|
@@ -233,9 +256,6 @@ function renderBody(result) {
|
|
|
233
256
|
return String(result.raw).trim();
|
|
234
257
|
}
|
|
235
258
|
|
|
236
|
-
if (result?.type === 'max-mode') {
|
|
237
|
-
return formatMaxModeOutput(result);
|
|
238
|
-
}
|
|
239
259
|
|
|
240
260
|
return String(result).trim();
|
|
241
261
|
}
|
|
@@ -282,24 +302,6 @@ function formatJsonOutput({ result, mode, body, tone, gate }) {
|
|
|
282
302
|
const scoreDetails = extractScoreDetails(result);
|
|
283
303
|
if (scoreDetails) payload.scores = scoreDetails;
|
|
284
304
|
|
|
285
|
-
if (result?.type === 'max-mode') {
|
|
286
|
-
payload.max = {
|
|
287
|
-
allFailed: Boolean(result.allFailed),
|
|
288
|
-
mpsFallback: Boolean(result.mpsFallback),
|
|
289
|
-
best: result.best ? {
|
|
290
|
-
model: result.best.model,
|
|
291
|
-
aiScore: result.best.aiScore ?? null,
|
|
292
|
-
mps: result.best.mps ?? null,
|
|
293
|
-
} : null,
|
|
294
|
-
candidates: result.candidates.map((candidate) => ({
|
|
295
|
-
model: candidate.model,
|
|
296
|
-
ok: Boolean(candidate.ok),
|
|
297
|
-
aiScore: candidate.aiScore ?? null,
|
|
298
|
-
mps: candidate.mps ?? null,
|
|
299
|
-
error: candidate.error ?? null,
|
|
300
|
-
})),
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
305
|
|
|
304
306
|
return JSON.stringify(payload, null, 2);
|
|
305
307
|
}
|
|
@@ -436,45 +438,55 @@ function normalizeFooterTail(lines) {
|
|
|
436
438
|
.trim();
|
|
437
439
|
}
|
|
438
440
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
+
/**
|
|
442
|
+
* Build a deterministic "backstop" section for audit mode. The LLM audit is
|
|
443
|
+
* model-dependent (a weak model silently drops 번역투/calques); these signals are
|
|
444
|
+
* computed deterministically so they appear regardless of which model ran. ko
|
|
445
|
+
* translationese rules are listed even below the hot-density gate, because audit
|
|
446
|
+
* is a hint surface, not a verdict.
|
|
447
|
+
*
|
|
448
|
+
* @param {string} text Source text.
|
|
449
|
+
* @param {object} [opts]
|
|
450
|
+
* @param {string} [opts.lang]
|
|
451
|
+
* @param {string} [opts.repoRoot]
|
|
452
|
+
* @returns {string} Markdown section (empty string when nothing fired).
|
|
453
|
+
*/
|
|
454
|
+
export function buildDeterministicAuditBackstop(text, opts = {}) {
|
|
455
|
+
const lang = opts.lang ?? 'ko';
|
|
456
|
+
const str = typeof text === 'string' ? text : '';
|
|
457
|
+
/** @type {Array<{signal:string,label:string,severity:string,location:string}>} */
|
|
458
|
+
const rows = [];
|
|
441
459
|
|
|
442
|
-
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const score = c.aiScore ?? '--';
|
|
452
|
-
const mps = c.mps ?? '--';
|
|
453
|
-
output += `| ${c.model} | ${score} | ${mps} | ${status} |\n`;
|
|
460
|
+
// ko translationese — per-rule, with matched samples (model-independent).
|
|
461
|
+
if (lang === 'ko' && str) {
|
|
462
|
+
for (const rule of TRANSLATIONESE_RULES) {
|
|
463
|
+
const matches = str.match(rule.re());
|
|
464
|
+
if (matches && matches.length) {
|
|
465
|
+
const samples = [...new Set(matches.map((m) => m.trim()).filter(Boolean))].slice(0, 4);
|
|
466
|
+
rows.push({ signal: `번역투: ${rule.id}`, label: rule.label, severity: rule.strong ? 'MEDIUM' : 'LOW', location: samples.join(', ') });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
454
469
|
}
|
|
455
470
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
} else if (result.mpsFallback) {
|
|
461
|
-
output += '⚠ No candidate passed MPS ≥ 70 — selecting by highest MPS (fallback)\n\n';
|
|
462
|
-
output += '> Exit code: 4.\n\n';
|
|
471
|
+
// markup leakage (near-proof) + density-gated discourse tells — language-agnostic.
|
|
472
|
+
const a = analyzeText(str, { lang, repoRoot: opts.repoRoot });
|
|
473
|
+
for (const h of a.markupLeakage?.hits ?? []) {
|
|
474
|
+
rows.push({ signal: 'markup-leakage', label: h.label, severity: 'HIGH', location: (h.samples ?? []).join(', ') });
|
|
463
475
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
output += '### Final Text\n\n';
|
|
467
|
-
output += best.result.trim();
|
|
468
|
-
output += '\n\n';
|
|
476
|
+
if (a.discourseTells?.fakeCandor?.hot) {
|
|
477
|
+
rows.push({ signal: 'discourse: fake-candor', label: '친근함 위장 도입부', severity: 'MEDIUM', location: (a.discourseTells.fakeCandor.hits ?? []).join(', ') });
|
|
469
478
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (c.model !== best?.model && c.ok && c.result) {
|
|
473
|
-
output += `\n<details>\n<summary>${c.model} result</summary>\n\n`;
|
|
474
|
-
output += c.result.trim();
|
|
475
|
-
output += '\n</details>\n';
|
|
476
|
-
}
|
|
479
|
+
if (a.discourseTells?.thematicBreaks?.hot) {
|
|
480
|
+
rows.push({ signal: 'discourse: thematic-breaks', label: '장식용 구분선 남용', severity: 'LOW', location: `${a.discourseTells.thematicBreaks.count}개` });
|
|
477
481
|
}
|
|
478
482
|
|
|
479
|
-
return
|
|
483
|
+
if (rows.length === 0) return '';
|
|
484
|
+
const lines = [
|
|
485
|
+
'## 결정적 신호 (deterministic backstop — 모델과 무관하게 항상 검사)',
|
|
486
|
+
'',
|
|
487
|
+
'| 신호 | 설명 | 심각도 | 위치 |',
|
|
488
|
+
'|------|------|--------|------|',
|
|
489
|
+
...rows.map((r) => `| ${r.signal} | ${r.label} | ${r.severity} | ${r.location} |`),
|
|
490
|
+
];
|
|
491
|
+
return `\n\n${lines.join('\n')}`;
|
|
480
492
|
}
|
package/src/prompt-builder.js
CHANGED
|
@@ -1,23 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Build the LLM prompt for rewrite, diff, audit, score, or ouroboros mode.
|
|
4
|
+
*
|
|
5
|
+
* @param {object} options Prompt inputs.
|
|
6
|
+
* @param {object} options.config Effective patina config.
|
|
7
|
+
* @param {object[]} options.patterns Loaded pattern packs.
|
|
8
|
+
* @param {object|null} options.profile Parsed profile document.
|
|
9
|
+
* @param {object|null} options.voice Parsed voice guide.
|
|
10
|
+
* @param {object|null} [options.voiceSample] Optional voice sample payload.
|
|
11
|
+
* @param {object|null} options.scoring Parsed scoring guide.
|
|
12
|
+
* @param {string} options.text Input text.
|
|
13
|
+
* @param {string} [options.mode=rewrite] Output mode.
|
|
14
|
+
* @param {object|null} [options.tone=null] Tone resolution metadata.
|
|
15
|
+
* @returns {string} Complete prompt text.
|
|
16
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
17
|
+
* @example
|
|
18
|
+
* const prompt = buildPrompt({ config, patterns, profile, voice, scoring, text: 'Draft' });
|
|
19
|
+
*/
|
|
20
|
+
export function buildPrompt(options) {
|
|
21
|
+
const {
|
|
22
|
+
config,
|
|
23
|
+
patterns,
|
|
24
|
+
profile,
|
|
25
|
+
voice,
|
|
26
|
+
voiceSample,
|
|
27
|
+
scoring,
|
|
28
|
+
text,
|
|
29
|
+
mode = 'rewrite',
|
|
30
|
+
tone = null,
|
|
31
|
+
} = options;
|
|
32
|
+
const promptMode = /** @type {any} */ (options).promptMode || 'strict';
|
|
33
|
+
// v3.11+ internal backend prompt-style dispatch. The compact prompt strips
|
|
34
|
+
// pattern definitions/examples and uses a casual instruction; it only applies
|
|
35
|
+
// to rewrite mode where voice prior matters most. Profile body is still passed
|
|
36
|
+
// through (Round 2 found Gemini ignored casual-conversation when omitted).
|
|
19
37
|
if (promptMode === 'minimal' && mode === 'rewrite') {
|
|
20
|
-
return buildMinimalPrompt({ config, patterns, profile, voiceSample, text, tone
|
|
38
|
+
return buildMinimalPrompt({ config, patterns, profile, voiceSample, text, tone });
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
const lang = config.language || 'ko';
|
|
@@ -100,7 +118,7 @@ export function buildPrompt({
|
|
|
100
118
|
prompt += `Process the following text according to the output mode "${mode}".\n\n`;
|
|
101
119
|
|
|
102
120
|
if (mode === 'rewrite') {
|
|
103
|
-
prompt += buildRewriteInstructions(structurePacks, lexicalPacks, {
|
|
121
|
+
prompt += buildRewriteInstructions(structurePacks, lexicalPacks, { lang });
|
|
104
122
|
} else if (mode === 'diff') {
|
|
105
123
|
prompt += buildDiffInstructions();
|
|
106
124
|
} else if (mode === 'audit') {
|
|
@@ -117,7 +135,7 @@ export function buildPrompt({
|
|
|
117
135
|
return prompt;
|
|
118
136
|
}
|
|
119
137
|
|
|
120
|
-
function buildRewriteInstructions(structurePacks, lexicalPacks, { includeSelfAudit = true,
|
|
138
|
+
function buildRewriteInstructions(structurePacks, lexicalPacks, { includeSelfAudit = true, lang = 'ko' } = {}) {
|
|
121
139
|
const phaseCount = includeSelfAudit ? 3 : 2;
|
|
122
140
|
let inst = `Follow the ${phaseCount}-Phase pipeline:\n\n`;
|
|
123
141
|
|
|
@@ -145,6 +163,11 @@ function buildRewriteInstructions(structurePacks, lexicalPacks, { includeSelfAud
|
|
|
145
163
|
inst += `4. Match profile tone\n`;
|
|
146
164
|
inst += `5. Inject personality per voice guidelines\n`;
|
|
147
165
|
inst += `6. Respect blocklist/allowlist and pattern overrides\n\n`;
|
|
166
|
+
const cjkGuard = buildCjkClauseRewriteGuard(lang);
|
|
167
|
+
if (cjkGuard) {
|
|
168
|
+
inst += `${cjkGuard}\n`;
|
|
169
|
+
}
|
|
170
|
+
|
|
148
171
|
|
|
149
172
|
if (includeSelfAudit) {
|
|
150
173
|
inst += `### Phase 3: Self-Audit\n\n`;
|
|
@@ -153,7 +176,7 @@ function buildRewriteInstructions(structurePacks, lexicalPacks, { includeSelfAud
|
|
|
153
176
|
inst += `3. Ensure Phase 1 corrections were not reverted in Phase 2\n`;
|
|
154
177
|
inst += `4. Final check: meaning preserved?\n\n`;
|
|
155
178
|
|
|
156
|
-
inst += buildOutputFormatBlock(
|
|
179
|
+
inst += buildOutputFormatBlock();
|
|
157
180
|
} else {
|
|
158
181
|
// Self-audit suppressed: external evaluators (scoreText, scoreMPS,
|
|
159
182
|
// scoreFidelity) handle AI-tell detection, polarity, and meaning checks
|
|
@@ -165,50 +188,55 @@ function buildRewriteInstructions(structurePacks, lexicalPacks, { includeSelfAud
|
|
|
165
188
|
return inst;
|
|
166
189
|
}
|
|
167
190
|
|
|
168
|
-
|
|
169
|
-
// [BODY]/[/BODY]; --variants > 1 uses [VARIANT n]/[/VARIANT] blocks.
|
|
170
|
-
function buildOutputFormatBlock({ variants = 1 } = {}) {
|
|
171
|
-
const isVariants = variants > 1;
|
|
172
|
-
const tag = isVariants ? '[VARIANT n]/[/VARIANT]' : '[BODY]/[/BODY]';
|
|
173
|
-
const itemDesc = isVariants
|
|
174
|
-
? `Produce ${variants} stylistic VARIANTS of the rewrite, each wrapped in ` +
|
|
175
|
-
`\`[VARIANT n]\`/\`[/VARIANT]\` tags where n is 1..${variants}. Each ` +
|
|
176
|
-
`variant must preserve all facts, numbers, and causation, but differ in ` +
|
|
177
|
-
`voice (e.g., V1 casual conversational, V2 direct/punchy, V3 measured/` +
|
|
178
|
-
`professional). No headings, no preamble inside the tags.`
|
|
179
|
-
: `The rewritten text wrapped in \`[BODY]\`/\`[/BODY]\` tags. The body ` +
|
|
180
|
-
`block must contain ONLY the user-facing rewrite — no headings, no ` +
|
|
181
|
-
`Phase labels, no preamble like "잔여 AI 티" or "최종 결과물".`;
|
|
182
|
-
const auditDesc = isVariants
|
|
183
|
-
? `(brief: what differs across variants, residual AI signals, applied patterns)`
|
|
184
|
-
: `(brief: what still looks AI-written, which patterns were applied). ` +
|
|
185
|
-
`This block is for downstream review — patina strips it before showing the user`;
|
|
186
|
-
|
|
187
|
-
let exampleBody = '';
|
|
188
|
-
if (isVariants) {
|
|
189
|
-
for (let i = 1; i <= variants; i++) {
|
|
190
|
-
exampleBody += `[VARIANT ${i}]\n<rewritten text — voice ${i}>\n[/VARIANT]\n\n`;
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
exampleBody = `[BODY]\n<rewritten text>\n[/BODY]\n\n`;
|
|
194
|
-
}
|
|
195
|
-
|
|
191
|
+
function buildOutputFormatBlock() {
|
|
196
192
|
return (
|
|
197
193
|
`### Output format (STRICT — v3.11)\n\n` +
|
|
198
194
|
`Produce output in this exact order, with no other text outside the tagged blocks:\n\n` +
|
|
199
|
-
`1.
|
|
200
|
-
|
|
195
|
+
`1. The rewritten text wrapped in \`[BODY]\`/\`[/BODY]\` tags. The body ` +
|
|
196
|
+
`block must contain ONLY the user-facing rewrite — no headings, no ` +
|
|
197
|
+
`Phase labels, no preamble like "잔여 AI 티" or "최종 결과물".\n` +
|
|
198
|
+
`2. Self-audit notes wrapped in \`[SELF_AUDIT]\`/\`[/SELF_AUDIT]\` tags ` +
|
|
199
|
+
`(brief: what still looks AI-written, which patterns were applied). ` +
|
|
200
|
+
`This block is for downstream review — patina strips it before showing the user.\n` +
|
|
201
201
|
`3. The Phase 6 YAML footer if tone resolution requires it.\n\n` +
|
|
202
|
-
`Example shape (uses
|
|
202
|
+
`Example shape (uses [BODY]/[/BODY]):\n\n` +
|
|
203
203
|
'```\n' +
|
|
204
|
-
|
|
205
|
-
`[SELF_AUDIT]\n-
|
|
206
|
-
`-
|
|
204
|
+
`[BODY]\n<rewritten text>\n[/BODY]\n\n` +
|
|
205
|
+
`[SELF_AUDIT]\n- residual signals: ...\n` +
|
|
206
|
+
`- patterns applied: ...\n[/SELF_AUDIT]\n\n` +
|
|
207
207
|
`---\ntone: ...\ntone_source: ...\ntone_evidence: [...]\ntone_confidence: ...\n---\n` +
|
|
208
208
|
'```\n'
|
|
209
209
|
);
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
function buildCjkClauseRewriteGuard(lang) {
|
|
213
|
+
if (!['ko', 'zh', 'ja'].includes(lang)) return '';
|
|
214
|
+
|
|
215
|
+
const shared = [
|
|
216
|
+
`### CJK clause-level rewrite guard`,
|
|
217
|
+
``,
|
|
218
|
+
`For Korean, Chinese, and Japanese, do not fix AI tells by swapping punctuation or single tokens in place. Read the full sentence, then rewrite the affected clause or sentence so the clause relationship is idiomatic in the target language.`,
|
|
219
|
+
`- If the suspect segment uses connective punctuation (em dash, colon, semicolon, slash, comma splice, parenthetical aside), choose a natural clause structure, sentence split, or connective phrase; do not replace every mark 1:1 with a comma or parentheses.`,
|
|
220
|
+
`- If a calque/translationese phrase is attached to punctuation, fix both together at clause level. Preserve who did what, polarity, conditions, numbers, and causation.`,
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
if (lang === 'ko') {
|
|
224
|
+
shared.push(
|
|
225
|
+
`- Korean examples: write "TUI 없이 완전 자율로 설치하려면 ..." rather than "무 TUI ..."; write "끝난 것 같아요"만으로는 부족한, 결과를 끝까지 확인해야 하는 열린 작업 rather than "끝난 것 같아요"로는 부족한 열린 작업.`
|
|
226
|
+
);
|
|
227
|
+
} else if (lang === 'zh') {
|
|
228
|
+
shared.push(
|
|
229
|
+
`- Chinese example: "不用 TUI 就能全自动安装时,打开自律模式参数" is preferable to a literal "无 TUI 设置"; an em dash should become a causal, contrastive, or appositive clause only when that relation is present.`
|
|
230
|
+
);
|
|
231
|
+
} else if (lang === 'ja') {
|
|
232
|
+
shared.push(
|
|
233
|
+
`- Japanese example: "TUIなしで完全自律インストールにしたい場合は..." is preferable to a literal calque; an em dash should become a natural 接続, 説明節, or sentence split only when the relation is present.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return `${shared.join('\n')}\n`;
|
|
238
|
+
}
|
|
239
|
+
|
|
212
240
|
function buildDiffInstructions() {
|
|
213
241
|
return `Show what changed and why, pattern by pattern. For each change use this exact label format:\n\n` +
|
|
214
242
|
`Pattern: N. Pattern Name\n` +
|
|
@@ -271,6 +299,15 @@ function buildScoreInstructions(config, lang, text = '') {
|
|
|
271
299
|
|
|
272
300
|
// v3.11 Phase 3.2 helper: classify a text as "short" for scoring boost.
|
|
273
301
|
// Threshold: ≤200 non-whitespace chars OR ≤3 non-empty paragraphs.
|
|
302
|
+
/**
|
|
303
|
+
* Classify whether text should use the short-text scoring boost.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} text Text to inspect.
|
|
306
|
+
* @returns {boolean} True when text is <=200 non-whitespace chars or <=3 paragraphs.
|
|
307
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
308
|
+
* @example
|
|
309
|
+
* const short = isShortText('A short note.');
|
|
310
|
+
*/
|
|
274
311
|
export function isShortText(text) {
|
|
275
312
|
if (!text) return true;
|
|
276
313
|
const stripped = text.replace(/\s+/g, '');
|
|
@@ -284,7 +321,7 @@ export function isShortText(text) {
|
|
|
284
321
|
// model's natural voice prior isn't overridden by analytical framing. Only
|
|
285
322
|
// invoked for rewrite mode; score/audit/diff/ouroboros stay on the strict
|
|
286
323
|
// path because they need precise pattern references.
|
|
287
|
-
function buildMinimalPrompt({ config, patterns, profile, voiceSample, text, tone
|
|
324
|
+
function buildMinimalPrompt({ config, patterns, profile, voiceSample, text, tone }) {
|
|
288
325
|
const lang = config.language || 'ko';
|
|
289
326
|
const activePatterns = patterns.filter((p) => !p.isScoreOnly);
|
|
290
327
|
|
|
@@ -302,6 +339,10 @@ function buildMinimalPrompt({ config, patterns, profile, voiceSample, text, tone
|
|
|
302
339
|
: `This text reads like AI. Rewrite it so it sounds like a real person wrote it. If you spot any of the phrases below, swap them out for something natural. Don't over-paraphrase — keep the meaning, numbers, and causation intact.`;
|
|
303
340
|
|
|
304
341
|
let prompt = `${instruction}\n\n`;
|
|
342
|
+
const cjkGuard = buildCjkClauseRewriteGuard(lang);
|
|
343
|
+
if (cjkGuard) {
|
|
344
|
+
prompt += `${cjkGuard}\n`;
|
|
345
|
+
}
|
|
305
346
|
|
|
306
347
|
if (watchWords.length > 0) {
|
|
307
348
|
prompt += lang === 'ko' ? `## AI 신호 어휘 (참고)\n\n` : `## AI signal words (reference)\n\n`;
|
|
@@ -328,16 +369,9 @@ function buildMinimalPrompt({ config, patterns, profile, voiceSample, text, tone
|
|
|
328
369
|
}
|
|
329
370
|
|
|
330
371
|
prompt += lang === 'ko' ? `## 출력 형식\n\n` : `## Output format\n\n`;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
prompt += `2. \`[SELF_AUDIT]\` ... \`[/SELF_AUDIT]\` 안에 짧게: variant별 voice 차이, 남은 AI 신호.\n`;
|
|
335
|
-
prompt += `3. 톤 정보가 있으면 마지막에 YAML 푸터.\n\n`;
|
|
336
|
-
} else {
|
|
337
|
-
prompt += `1. 다듬은 본문을 \`[BODY]\` ... \`[/BODY]\` 안에. 본문만, 머리말·메타·"최종 결과물" 같은 라벨 없이.\n`;
|
|
338
|
-
prompt += `2. \`[SELF_AUDIT]\` ... \`[/SELF_AUDIT]\` 안에 짧게: 어떤 부분 손봤는지, 남은 AI 신호 있는지.\n`;
|
|
339
|
-
prompt += `3. 톤 정보가 있으면 마지막에 YAML 푸터: \`---\\ntone: ...\\ntone_source: ...\\ntone_evidence: [...]\\ntone_confidence: ...\\n---\`\n\n`;
|
|
340
|
-
}
|
|
372
|
+
prompt += `1. 다듬은 본문을 \`[BODY]\` ... \`[/BODY]\` 안에. 본문만, 머리말·메타·"최종 결과물" 같은 라벨 없이.\n`;
|
|
373
|
+
prompt += `2. \`[SELF_AUDIT]\` ... \`[/SELF_AUDIT]\` 안에 짧게: 어떤 부분 손봤는지, 남은 AI 신호 있는지.\n`;
|
|
374
|
+
prompt += `3. 톤 정보가 있으면 마지막에 YAML 푸터: \`---\\ntone: ...\\ntone_source: ...\\ntone_evidence: [...]\\ntone_confidence: ...\\n---\`\n\n`;
|
|
341
375
|
|
|
342
376
|
prompt += lang === 'ko' ? `## 입력\n\n${text}\n\n` : `## Input\n\n${text}\n\n`;
|
|
343
377
|
prompt += lang === 'ko' ? `## 출력\n\n` : `## Output\n\n`;
|
|
@@ -385,6 +419,7 @@ function buildOuroborosInstructions(config, structurePacks, lexicalPacks) {
|
|
|
385
419
|
const fidelityFloor = ouroboros['fidelity-floor'] ?? 70;
|
|
386
420
|
const mpsFloor = ouroboros['mps-floor'] ?? 70;
|
|
387
421
|
|
|
422
|
+
const lang = config.language || 'ko';
|
|
388
423
|
let inst = `Iterative self-improvement loop:\n\n`;
|
|
389
424
|
inst += `1. Measure initial AI-likeness score\n`;
|
|
390
425
|
inst += `2. If score ≤ ${targetScore}, stop immediately\n`;
|
|
@@ -403,7 +438,7 @@ function buildOuroborosInstructions(config, structurePacks, lexicalPacks) {
|
|
|
403
438
|
// Skip Phase 3 self-audit: each iteration runs through external evaluators
|
|
404
439
|
// (scoreText, scoreMPS, scoreFidelity) in src/ouroboros.js, so an in-prompt
|
|
405
440
|
// self-audit duplicates work and inflates token cost.
|
|
406
|
-
inst += buildRewriteInstructions(structurePacks, lexicalPacks, { includeSelfAudit: false });
|
|
441
|
+
inst += buildRewriteInstructions(structurePacks, lexicalPacks, { includeSelfAudit: false, lang });
|
|
407
442
|
|
|
408
443
|
return inst;
|
|
409
444
|
}
|