patina-cli 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.patina.default.yaml +211 -0
- package/CHANGELOG.md +265 -0
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/README_JA.md +254 -0
- package/README_KR.md +253 -0
- package/README_ZH.md +254 -0
- package/SKILL-MAX.md +455 -0
- package/SKILL.md +730 -0
- package/assets/brand/patina-icon.svg +9 -0
- package/assets/brand/patina-logo.svg +17 -0
- package/assets/social/patina-before-after.svg +46 -0
- package/assets/social/patina-og.svg +31 -0
- package/bin/patina.js +9 -0
- package/core/scoring.md +657 -0
- package/core/standalone-prompt.md +364 -0
- package/core/stylometry.md +754 -0
- package/core/voice.md +163 -0
- package/docs/AUTHENTICATION.md +105 -0
- package/docs/AUTHENTICATION_KR.md +105 -0
- package/docs/BRANDING.md +37 -0
- package/docs/CLI.md +80 -0
- package/docs/COMPARISON.md +38 -0
- package/docs/COOKBOOK.md +173 -0
- package/docs/DEMO.md +40 -0
- package/docs/ETHICS.md +27 -0
- package/docs/EXAMPLES.md +130 -0
- package/docs/EXAMPLES_KR.md +130 -0
- package/docs/EXIT-CODES.md +25 -0
- package/docs/FAQ.md +67 -0
- package/docs/FAQ_KR.md +65 -0
- package/docs/FLAG-PARITY.md +53 -0
- package/docs/GLOSSARY.md +123 -0
- package/docs/PATTERNS-EN.md +718 -0
- package/docs/PATTERNS-JA.md +706 -0
- package/docs/PATTERNS-KO.md +707 -0
- package/docs/PATTERNS-ZH.md +706 -0
- package/docs/PATTERNS.md +22 -0
- package/docs/ROADMAP.md +315 -0
- package/docs/audits/2026-05-deep-research.md +290 -0
- package/docs/benchmarks/detector-comparison.json +442 -0
- package/docs/benchmarks/detector-comparison.md +65 -0
- package/docs/benchmarks/latest.json +988 -0
- package/docs/benchmarks/latest.md +112 -0
- package/docs/integrations/docker.md +19 -0
- package/docs/integrations/github-action.md +59 -0
- package/docs/integrations/pre-commit.md +77 -0
- package/docs/integrations/release.md +43 -0
- package/docs/internal/HARNESS.md +14 -0
- package/docs/internal/README.md +14 -0
- package/docs/internal/WARP.md +23 -0
- package/docs/research/2025-rebaseline-plan.md +89 -0
- package/docs/research/ai-human-metrics.md +380 -0
- package/docs/social/gstack-cardnews.html +236 -0
- package/docs/social/gstack-cardnews.md +88 -0
- package/docs/social/gstack-thread.md +106 -0
- package/docs/social/patina-launch-copy.md +227 -0
- package/docs/superpowers/specs/2026-04-03-meaning-preservation-design.md +299 -0
- package/lexicon/ai-en.md +162 -0
- package/lexicon/ai-ko.md +159 -0
- package/package.json +100 -0
- package/patina-max/SKILL.md +523 -0
- package/patina-max/composite.py +457 -0
- package/patterns/en-communication.md +89 -0
- package/patterns/en-content.md +133 -0
- package/patterns/en-filler.md +113 -0
- package/patterns/en-language.md +163 -0
- package/patterns/en-structure.md +173 -0
- package/patterns/en-style.md +139 -0
- package/patterns/en-viral-hook.md +211 -0
- package/patterns/ja-communication.md +101 -0
- package/patterns/ja-content.md +153 -0
- package/patterns/ja-filler.md +123 -0
- package/patterns/ja-language.md +190 -0
- package/patterns/ja-structure.md +142 -0
- package/patterns/ja-style.md +147 -0
- package/patterns/ja-viral-hook.md +216 -0
- package/patterns/ko-communication.md +98 -0
- package/patterns/ko-content.md +154 -0
- package/patterns/ko-filler.md +105 -0
- package/patterns/ko-language.md +182 -0
- package/patterns/ko-structure.md +147 -0
- package/patterns/ko-style.md +146 -0
- package/patterns/ko-viral-hook.md +211 -0
- package/patterns/zh-communication.md +101 -0
- package/patterns/zh-content.md +153 -0
- package/patterns/zh-filler.md +118 -0
- package/patterns/zh-language.md +173 -0
- package/patterns/zh-structure.md +145 -0
- package/patterns/zh-style.md +159 -0
- package/patterns/zh-viral-hook.md +216 -0
- package/profiles/academic.md +53 -0
- package/profiles/blog.md +81 -0
- package/profiles/casual-conversation.md +105 -0
- package/profiles/code-comment.md +104 -0
- package/profiles/commit-message.md +99 -0
- package/profiles/default.md +62 -0
- package/profiles/email.md +52 -0
- package/profiles/formal.md +98 -0
- package/profiles/instructional.md +80 -0
- package/profiles/legal.md +57 -0
- package/profiles/marketing.md +56 -0
- package/profiles/medical.md +53 -0
- package/profiles/narrative.md +79 -0
- package/profiles/release-notes.md +98 -0
- package/profiles/social.md +56 -0
- package/profiles/technical.md +53 -0
- package/scripts/benchmark-report.mjs +252 -0
- package/scripts/check-release-metadata.mjs +48 -0
- package/scripts/detector-comparison.mjs +267 -0
- package/scripts/lint.mjs +40 -0
- package/scripts/precommit-score.mjs +31 -0
- package/scripts/prose-score.mjs +186 -0
- package/scripts/update-benchmark-ranges.mjs +108 -0
- package/src/api.js +330 -0
- package/src/auth.js +105 -0
- package/src/backends/claude-cli.js +112 -0
- package/src/backends/codex-cli.js +121 -0
- package/src/backends/contract.js +21 -0
- package/src/backends/gemini-cli.js +135 -0
- package/src/backends/index.js +159 -0
- package/src/cache.js +106 -0
- package/src/cli.js +1280 -0
- package/src/commands/doctor.js +229 -0
- package/src/commands/init.js +208 -0
- package/src/config.js +126 -0
- package/src/errors.js +53 -0
- package/src/features/index.js +96 -0
- package/src/features/lexicon.js +90 -0
- package/src/features/segment.js +49 -0
- package/src/features/stylometry.js +50 -0
- package/src/loader.js +103 -0
- package/src/logger.js +70 -0
- package/src/manifest.js +162 -0
- package/src/max-mode.js +207 -0
- package/src/ouroboros.js +233 -0
- package/src/output.js +480 -0
- package/src/prompt-builder.js +409 -0
- package/src/providers.js +100 -0
- package/src/scoring.js +531 -0
- package/src/security.js +133 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-01.md +16 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-02.md +16 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-03.md +17 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-04.md +15 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-05.md +16 -0
- package/tests/fixtures/suspect-zones/en/ai/en-ai-06-chat-register.md +16 -0
- package/tests/fixtures/suspect-zones/en/natural/en-nat-01.md +15 -0
- package/tests/fixtures/suspect-zones/en/natural/en-nat-02.md +15 -0
- package/tests/fixtures/suspect-zones/en/natural/en-nat-03.md +15 -0
- package/tests/fixtures/suspect-zones/en/natural/en-nat-04.md +15 -0
- package/tests/fixtures/suspect-zones/en/natural/en-nat-05.md +15 -0
- package/tests/fixtures/suspect-zones/expected-ranges.json +939 -0
- package/tests/fixtures/suspect-zones/ja/ai/ja-ai-01.md +11 -0
- package/tests/fixtures/suspect-zones/ja/ai/ja-ai-02.md +11 -0
- package/tests/fixtures/suspect-zones/ja/ai/ja-ai-03.md +11 -0
- package/tests/fixtures/suspect-zones/ja/natural/ja-nat-01.md +11 -0
- package/tests/fixtures/suspect-zones/ja/natural/ja-nat-02.md +11 -0
- package/tests/fixtures/suspect-zones/ja/natural/ja-nat-03.md +11 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-01.md +14 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-02.md +16 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-03.md +15 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-04.md +15 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-05.md +16 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-06-chat-register.md +16 -0
- package/tests/fixtures/suspect-zones/ko/natural/ko-nat-01.md +15 -0
- package/tests/fixtures/suspect-zones/ko/natural/ko-nat-02.md +15 -0
- package/tests/fixtures/suspect-zones/ko/natural/ko-nat-03.md +15 -0
- package/tests/fixtures/suspect-zones/ko/natural/ko-nat-04.md +14 -0
- package/tests/fixtures/suspect-zones/ko/natural/ko-nat-05.md +15 -0
- package/tests/fixtures/suspect-zones/zh/ai/zh-ai-01.md +11 -0
- package/tests/fixtures/suspect-zones/zh/ai/zh-ai-02.md +11 -0
- package/tests/fixtures/suspect-zones/zh/ai/zh-ai-03.md +11 -0
- package/tests/fixtures/suspect-zones/zh/natural/zh-nat-01.md +11 -0
- package/tests/fixtures/suspect-zones/zh/natural/zh-nat-02.md +11 -0
- package/tests/fixtures/suspect-zones/zh/natural/zh-nat-03.md +11 -0
- package/tests/quality/README.md +121 -0
- package/tests/quality/benchmark.mjs +306 -0
- package/tests/quality/detectors.manual.example.json +31 -0
- package/tests/quality/dogfood.mjs +44 -0
package/src/max-mode.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { callLLM as defaultCallLLM, callLLMMultiple } from './api.js';
|
|
2
|
+
import { scoreText, scoreMPS } from './scoring.js';
|
|
3
|
+
import { createLogger } from './logger.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_WALL_CLOCK_BUDGET_MS = 300_000;
|
|
6
|
+
|
|
7
|
+
export async function runMaxMode({
|
|
8
|
+
prompt,
|
|
9
|
+
sourceText,
|
|
10
|
+
models,
|
|
11
|
+
apiKey,
|
|
12
|
+
baseURL,
|
|
13
|
+
config,
|
|
14
|
+
patterns,
|
|
15
|
+
maxConcurrency,
|
|
16
|
+
wallClockBudgetMs = DEFAULT_WALL_CLOCK_BUDGET_MS,
|
|
17
|
+
callLLM = defaultCallLLM,
|
|
18
|
+
now = () => Date.now(),
|
|
19
|
+
sleep,
|
|
20
|
+
callLLMMultipleImpl = callLLMMultiple,
|
|
21
|
+
scoreTextImpl = scoreText,
|
|
22
|
+
scoreMPSImpl = scoreMPS,
|
|
23
|
+
signal,
|
|
24
|
+
logger = createLogger(),
|
|
25
|
+
}) {
|
|
26
|
+
logger.info('max.dispatch', {
|
|
27
|
+
message: `[patina-max] Dispatching to ${models.length} models: ${models.join(', ')}`,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const abortFromCaller = () => controller.abort();
|
|
32
|
+
let cleanupCallerSignal = () => {};
|
|
33
|
+
if (signal) {
|
|
34
|
+
if (signal.aborted) {
|
|
35
|
+
controller.abort();
|
|
36
|
+
} else {
|
|
37
|
+
signal.addEventListener('abort', abortFromCaller, { once: true });
|
|
38
|
+
cleanupCallerSignal = () => signal.removeEventListener('abort', abortFromCaller);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const deadline = now() + wallClockBudgetMs;
|
|
42
|
+
const progressStartedAt = now();
|
|
43
|
+
const modelStatus = new Map(models.map((model) => [model, '...']));
|
|
44
|
+
const modelStartedAt = new Map();
|
|
45
|
+
let timedOut = false;
|
|
46
|
+
const timeout = setTimeout(() => {
|
|
47
|
+
timedOut = true;
|
|
48
|
+
controller.abort();
|
|
49
|
+
logger.warn('max.timeout', { message: '[patina-max] MAX wall-clock timeout reached; returning partial results' });
|
|
50
|
+
}, wallClockBudgetMs);
|
|
51
|
+
|
|
52
|
+
const renderProgress = () => {
|
|
53
|
+
const statuses = models.map((model) => `${model} ${modelStatus.get(model) || '...'}`).join(' ');
|
|
54
|
+
const elapsedSeconds = Math.max(0, Math.round((now() - progressStartedAt) / 1000));
|
|
55
|
+
logger.progress('max.progress', {
|
|
56
|
+
message: `[patina-max] ${statuses} (${elapsedSeconds}s)`,
|
|
57
|
+
elapsed_ms: Math.max(0, now() - progressStartedAt),
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const candidates = [];
|
|
62
|
+
try {
|
|
63
|
+
const results = await callLLMMultipleImpl({
|
|
64
|
+
prompt,
|
|
65
|
+
models,
|
|
66
|
+
apiKey,
|
|
67
|
+
baseURL,
|
|
68
|
+
maxConcurrency,
|
|
69
|
+
deadline,
|
|
70
|
+
signal: controller.signal,
|
|
71
|
+
callLLM,
|
|
72
|
+
now,
|
|
73
|
+
sleep,
|
|
74
|
+
onStart: (model) => {
|
|
75
|
+
modelStartedAt.set(model, now());
|
|
76
|
+
modelStatus.set(model, '...');
|
|
77
|
+
renderProgress();
|
|
78
|
+
},
|
|
79
|
+
onComplete: (model, ok) => {
|
|
80
|
+
const latencyMs = modelStartedAt.has(model) ? Math.max(0, now() - modelStartedAt.get(model)) : undefined;
|
|
81
|
+
modelStatus.set(model, ok ? '✓' : '✗');
|
|
82
|
+
logger.progress('max.model_complete', {
|
|
83
|
+
message: formatMaxProgress(models, modelStatus, progressStartedAt, now),
|
|
84
|
+
model,
|
|
85
|
+
latency_ms: latencyMs,
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
for (const r of results) {
|
|
91
|
+
if (!r.ok) {
|
|
92
|
+
candidates.push({ model: r.model, ok: false, error: r.error });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let aiScoreResult = null;
|
|
97
|
+
let mpsResult = null;
|
|
98
|
+
|
|
99
|
+
if (!timedOut) {
|
|
100
|
+
aiScoreResult = await scoreTextImpl({
|
|
101
|
+
text: r.result,
|
|
102
|
+
config,
|
|
103
|
+
patterns,
|
|
104
|
+
apiKey,
|
|
105
|
+
baseURL,
|
|
106
|
+
model: r.model,
|
|
107
|
+
deadline,
|
|
108
|
+
signal: controller.signal,
|
|
109
|
+
callLLM,
|
|
110
|
+
logger,
|
|
111
|
+
now,
|
|
112
|
+
sleep,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!timedOut) {
|
|
117
|
+
mpsResult = await scoreMPSImpl({
|
|
118
|
+
original: sourceText,
|
|
119
|
+
rewritten: r.result,
|
|
120
|
+
apiKey,
|
|
121
|
+
baseURL,
|
|
122
|
+
model: r.model,
|
|
123
|
+
deadline,
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
callLLM,
|
|
126
|
+
logger,
|
|
127
|
+
now,
|
|
128
|
+
sleep,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
candidates.push({
|
|
133
|
+
model: r.model,
|
|
134
|
+
ok: true,
|
|
135
|
+
result: r.result,
|
|
136
|
+
aiScore: aiScoreResult?.overall ?? null,
|
|
137
|
+
mps: mpsResult?.mps ?? null,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (timedOut) break;
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
clearTimeout(timeout);
|
|
144
|
+
cleanupCallerSignal();
|
|
145
|
+
logger.closeProgress();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { candidate: best, fallback } = selectBest(candidates, {
|
|
149
|
+
log: (message) => logger.warn('max.selection_tie', { message }),
|
|
150
|
+
});
|
|
151
|
+
const allFailed = best === null;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
type: 'max-mode',
|
|
155
|
+
candidates,
|
|
156
|
+
best,
|
|
157
|
+
allFailed,
|
|
158
|
+
mpsFallback: fallback,
|
|
159
|
+
timedOut,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function selectBest(
|
|
164
|
+
candidates,
|
|
165
|
+
{ log = (message) => createLogger().warn('max.selection_tie', { message }) } = {}
|
|
166
|
+
) {
|
|
167
|
+
const valid = candidates.filter((c) => c.ok && c.aiScore !== null);
|
|
168
|
+
|
|
169
|
+
if (valid.length === 0) {
|
|
170
|
+
return { candidate: null, fallback: false };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const passingMps = valid.filter((c) => (c.mps ?? 0) >= 70);
|
|
174
|
+
|
|
175
|
+
if (passingMps.length > 0) {
|
|
176
|
+
const best = passingMps.reduce((best, current) =>
|
|
177
|
+
// Strict comparison preserves --models config order when AI scores tie.
|
|
178
|
+
(current.aiScore < best.aiScore) ? current : best
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (passingMps.some((c) => c !== best && c.aiScore === best.aiScore)) {
|
|
182
|
+
log(`[patina-max] Tie on AI score — picked ${best.model} by config order`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { candidate: best, fallback: false };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const best = valid.reduce((best, current) => {
|
|
189
|
+
const bestMps = best.mps ?? -1;
|
|
190
|
+
const currentMps = current.mps ?? -1;
|
|
191
|
+
// Strict comparison preserves --models config order when MPS scores tie.
|
|
192
|
+
return currentMps > bestMps ? current : best;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const bestMps = best.mps ?? -1;
|
|
196
|
+
if (valid.some((c) => c !== best && (c.mps ?? -1) === bestMps)) {
|
|
197
|
+
log(`[patina-max] Tie on MPS — picked ${best.model} by config order`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { candidate: best, fallback: true };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function formatMaxProgress(models, modelStatus, startedAt, now) {
|
|
204
|
+
const statuses = models.map((model) => `${model} ${modelStatus.get(model) || '...'}`).join(' ');
|
|
205
|
+
const elapsedSeconds = Math.max(0, Math.round((now() - startedAt) / 1000));
|
|
206
|
+
return `[patina-max] ${statuses} (${elapsedSeconds}s)`;
|
|
207
|
+
}
|
package/src/ouroboros.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { callLLM as defaultCallLLM } from './api.js';
|
|
2
|
+
import { scoreText, scoreMPS, scoreFidelity, combinedScore } from './scoring.js';
|
|
3
|
+
import { buildPrompt } from './prompt-builder.js';
|
|
4
|
+
import { createLogger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
export async function runOuroboros({
|
|
7
|
+
config,
|
|
8
|
+
patterns,
|
|
9
|
+
profile,
|
|
10
|
+
voice,
|
|
11
|
+
voiceSample,
|
|
12
|
+
scoring,
|
|
13
|
+
text,
|
|
14
|
+
apiKey,
|
|
15
|
+
baseURL,
|
|
16
|
+
model,
|
|
17
|
+
callLLM = defaultCallLLM,
|
|
18
|
+
now,
|
|
19
|
+
sleep,
|
|
20
|
+
signal,
|
|
21
|
+
logger = createLogger(),
|
|
22
|
+
}) {
|
|
23
|
+
const ouroborosConfig = config.ouroboros || {};
|
|
24
|
+
const targetScore = ouroborosConfig['target-score'] ?? 30;
|
|
25
|
+
const maxIterations = ouroborosConfig['max-iterations'] ?? 3;
|
|
26
|
+
const plateauThreshold = ouroborosConfig['plateau-threshold'] ?? 10;
|
|
27
|
+
const fidelityFloor = ouroborosConfig['fidelity-floor'] ?? 70;
|
|
28
|
+
const mpsFloor = ouroborosConfig['mps-floor'] ?? 70;
|
|
29
|
+
|
|
30
|
+
const iterationLog = [];
|
|
31
|
+
|
|
32
|
+
const initialScoreResult = await scoreText({
|
|
33
|
+
text,
|
|
34
|
+
config,
|
|
35
|
+
patterns,
|
|
36
|
+
apiKey,
|
|
37
|
+
baseURL,
|
|
38
|
+
model,
|
|
39
|
+
callLLM,
|
|
40
|
+
now,
|
|
41
|
+
sleep,
|
|
42
|
+
signal,
|
|
43
|
+
logger,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const initialScore = initialScoreResult?.overall ?? 100;
|
|
47
|
+
|
|
48
|
+
iterationLog.push({
|
|
49
|
+
iteration: 0,
|
|
50
|
+
before: null,
|
|
51
|
+
after: initialScore,
|
|
52
|
+
improvement: null,
|
|
53
|
+
reason: 'Initial',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (initialScore <= targetScore) {
|
|
57
|
+
return {
|
|
58
|
+
finalText: text,
|
|
59
|
+
finalScore: initialScore,
|
|
60
|
+
iterations: 0,
|
|
61
|
+
reason: `Already at target (score: ${initialScore})`,
|
|
62
|
+
log: iterationLog,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let currentText = text;
|
|
67
|
+
let previousScore = initialScore;
|
|
68
|
+
let bestText = text;
|
|
69
|
+
let bestScore = initialScore;
|
|
70
|
+
// Original is identical to itself, so its fidelity is 100 by definition.
|
|
71
|
+
// This gives iteration 1 a valid combined baseline to detect regressions against.
|
|
72
|
+
let previousCombined = combinedScore({
|
|
73
|
+
aiLikeness: initialScore,
|
|
74
|
+
fidelity: 100,
|
|
75
|
+
profile: config.profile,
|
|
76
|
+
config,
|
|
77
|
+
deterministicScore: initialScoreResult?.deterministicScore,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
81
|
+
const prompt = buildPrompt({
|
|
82
|
+
config,
|
|
83
|
+
patterns,
|
|
84
|
+
profile,
|
|
85
|
+
voice,
|
|
86
|
+
voiceSample,
|
|
87
|
+
scoring,
|
|
88
|
+
text: currentText,
|
|
89
|
+
mode: 'rewrite',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const iterationStartedAt = now ? now() : Date.now();
|
|
93
|
+
const humanized = await callLLM({
|
|
94
|
+
prompt,
|
|
95
|
+
apiKey,
|
|
96
|
+
baseURL,
|
|
97
|
+
model,
|
|
98
|
+
now,
|
|
99
|
+
sleep,
|
|
100
|
+
signal,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const scoreResult = await scoreText({
|
|
104
|
+
text: humanized,
|
|
105
|
+
config,
|
|
106
|
+
patterns,
|
|
107
|
+
apiKey,
|
|
108
|
+
baseURL,
|
|
109
|
+
model,
|
|
110
|
+
callLLM,
|
|
111
|
+
now,
|
|
112
|
+
sleep,
|
|
113
|
+
signal,
|
|
114
|
+
logger,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
let currentScore = scoreResult?.overall ?? 100;
|
|
118
|
+
const delta = previousScore - currentScore;
|
|
119
|
+
const latencyMs = Math.max(0, (now ? now() : Date.now()) - iterationStartedAt);
|
|
120
|
+
logger.info('ouroboros.iteration', {
|
|
121
|
+
message: `[ouroboros] iter ${iteration}/${maxIterations} score ${previousScore} → ${currentScore} (${formatElapsed(latencyMs)})`,
|
|
122
|
+
model,
|
|
123
|
+
latency_ms: latencyMs,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const [mpsResult, fidelityResult] = await Promise.all([
|
|
127
|
+
scoreMPS({
|
|
128
|
+
original: text,
|
|
129
|
+
rewritten: humanized,
|
|
130
|
+
apiKey,
|
|
131
|
+
baseURL,
|
|
132
|
+
model,
|
|
133
|
+
callLLM,
|
|
134
|
+
now,
|
|
135
|
+
sleep,
|
|
136
|
+
signal,
|
|
137
|
+
logger,
|
|
138
|
+
}),
|
|
139
|
+
scoreFidelity({
|
|
140
|
+
original: text,
|
|
141
|
+
rewritten: humanized,
|
|
142
|
+
apiKey,
|
|
143
|
+
baseURL,
|
|
144
|
+
model,
|
|
145
|
+
callLLM,
|
|
146
|
+
now,
|
|
147
|
+
sleep,
|
|
148
|
+
signal,
|
|
149
|
+
logger,
|
|
150
|
+
}),
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const mps = mpsResult?.mps ?? 100;
|
|
154
|
+
const fidelity = fidelityResult?.fidelity ?? 100;
|
|
155
|
+
const combined = combinedScore({
|
|
156
|
+
aiLikeness: currentScore,
|
|
157
|
+
fidelity,
|
|
158
|
+
profile: config.profile,
|
|
159
|
+
config,
|
|
160
|
+
deterministicScore: scoreResult?.deterministicScore,
|
|
161
|
+
});
|
|
162
|
+
const combinedDelta = previousCombined - combined;
|
|
163
|
+
|
|
164
|
+
let reason = '';
|
|
165
|
+
let shouldStop = false;
|
|
166
|
+
let shouldRollback = false;
|
|
167
|
+
|
|
168
|
+
if (currentScore <= targetScore) {
|
|
169
|
+
reason = 'Target met';
|
|
170
|
+
shouldStop = true;
|
|
171
|
+
} else if (combinedDelta < 0) {
|
|
172
|
+
reason = `Regression (combined ${previousCombined} → ${combined})`;
|
|
173
|
+
shouldStop = true;
|
|
174
|
+
shouldRollback = true;
|
|
175
|
+
} else if (fidelity < fidelityFloor) {
|
|
176
|
+
reason = 'Fidelity floor violation';
|
|
177
|
+
shouldStop = true;
|
|
178
|
+
shouldRollback = true;
|
|
179
|
+
} else if (mps < mpsFloor) {
|
|
180
|
+
reason = 'MPS floor violation';
|
|
181
|
+
shouldStop = true;
|
|
182
|
+
shouldRollback = true;
|
|
183
|
+
} else if (delta <= plateauThreshold) {
|
|
184
|
+
reason = 'Plateau';
|
|
185
|
+
shouldStop = true;
|
|
186
|
+
} else if (iteration >= maxIterations) {
|
|
187
|
+
reason = 'Max iterations';
|
|
188
|
+
shouldStop = true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
iterationLog.push({
|
|
192
|
+
iteration,
|
|
193
|
+
before: previousScore,
|
|
194
|
+
after: currentScore,
|
|
195
|
+
improvement: delta,
|
|
196
|
+
fidelity,
|
|
197
|
+
mps,
|
|
198
|
+
combined,
|
|
199
|
+
combinedDelta,
|
|
200
|
+
reason: shouldStop ? reason : '',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (shouldRollback) {
|
|
204
|
+
currentText = bestText;
|
|
205
|
+
currentScore = bestScore;
|
|
206
|
+
} else {
|
|
207
|
+
currentText = humanized;
|
|
208
|
+
previousScore = currentScore;
|
|
209
|
+
previousCombined = combined;
|
|
210
|
+
|
|
211
|
+
if (currentScore < bestScore) {
|
|
212
|
+
bestText = humanized;
|
|
213
|
+
bestScore = currentScore;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (shouldStop) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
finalText: currentText,
|
|
224
|
+
finalScore: bestScore,
|
|
225
|
+
iterations: iterationLog.length - 1,
|
|
226
|
+
reason: iterationLog[iterationLog.length - 1]?.reason || 'Completed',
|
|
227
|
+
log: iterationLog,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function formatElapsed(ms) {
|
|
232
|
+
return `${Math.round(ms / 100) / 10}s`;
|
|
233
|
+
}
|