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/commands/init.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { createInterface } from 'node:readline/promises';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { stdin as input, stdout as output } from 'node:process';
|
|
6
|
-
import yaml from 'js-yaml';
|
|
7
|
-
import { listBackends } from '../backends/index.js';
|
|
8
|
-
import { inputError } from '../errors.js';
|
|
9
|
-
import { createLogger } from '../logger.js';
|
|
10
|
-
|
|
11
|
-
const LANGUAGES = ['ko', 'en', 'zh', 'ja'];
|
|
12
|
-
const PROFILES = [
|
|
13
|
-
'default',
|
|
14
|
-
'blog',
|
|
15
|
-
'academic',
|
|
16
|
-
'technical',
|
|
17
|
-
'formal',
|
|
18
|
-
'social',
|
|
19
|
-
'email',
|
|
20
|
-
'legal',
|
|
21
|
-
'medical',
|
|
22
|
-
'marketing',
|
|
23
|
-
'narrative',
|
|
24
|
-
'instructional',
|
|
25
|
-
'casual-conversation',
|
|
26
|
-
'code-comment',
|
|
27
|
-
'commit-message',
|
|
28
|
-
'release-notes',
|
|
29
|
-
];
|
|
30
|
-
const TONES = ['profile-only', 'casual', 'professional', 'academic', 'narrative', 'marketing', 'instructional', 'auto'];
|
|
31
|
-
const DISPATCH_MODES = ['omc', 'direct', 'api'];
|
|
32
|
-
|
|
33
|
-
export async function runInit(args = [], { logger = createLogger() } = {}) {
|
|
34
|
-
const parsed = parseInitArgs(args);
|
|
35
|
-
if (parsed.help) {
|
|
36
|
-
printInitHelp();
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const target = resolve(process.cwd(), '.patina.yaml');
|
|
41
|
-
if (existsSync(target) && !parsed.force) {
|
|
42
|
-
if (parsed.defaults || !process.stdin.isTTY) {
|
|
43
|
-
throw inputError(
|
|
44
|
-
'.patina.yaml already exists',
|
|
45
|
-
'init will not overwrite an existing project config without confirmation.',
|
|
46
|
-
'Run `patina init --force` to replace it, or edit .patina.yaml manually.'
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const detected = detectInitDefaults();
|
|
52
|
-
const answers = parsed.defaults
|
|
53
|
-
? detected
|
|
54
|
-
: await promptForConfig(detected, { target, force: parsed.force, logger });
|
|
55
|
-
|
|
56
|
-
if (!answers) {
|
|
57
|
-
logger.info('init.kept_existing', { message: '[patina] kept existing .patina.yaml' });
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const config = buildInitConfig(answers);
|
|
62
|
-
writeFileSync(target, `${yaml.dump(config, { lineWidth: 100 }).trimEnd()}\n`, 'utf8');
|
|
63
|
-
console.log(`[patina] wrote ${target}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function detectInitDefaults() {
|
|
67
|
-
const backends = listBackends();
|
|
68
|
-
const authenticated = backends.filter((b) => b.available && b.authenticated);
|
|
69
|
-
const preferredBackend = (
|
|
70
|
-
authenticated.find((b) => b.name !== 'openai-http') ||
|
|
71
|
-
authenticated[0] ||
|
|
72
|
-
backends.find((b) => b.name === 'openai-http')
|
|
73
|
-
)?.name || 'openai-http';
|
|
74
|
-
|
|
75
|
-
const maxModels = authenticated
|
|
76
|
-
.map((b) => MODEL_BY_BACKEND[b.name])
|
|
77
|
-
.filter(Boolean);
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
language: 'ko',
|
|
81
|
-
profile: 'default',
|
|
82
|
-
tone: 'profile-only',
|
|
83
|
-
backend: preferredBackend,
|
|
84
|
-
maxModels: maxModels.length > 0 ? maxModels : ['claude', 'gemini'],
|
|
85
|
-
dispatch: commandAvailable('tmux') ? 'omc' : 'direct',
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function parseInitArgs(args) {
|
|
90
|
-
const parsed = {};
|
|
91
|
-
for (let i = 0; i < args.length; i++) {
|
|
92
|
-
const arg = args[i];
|
|
93
|
-
switch (arg) {
|
|
94
|
-
case '--help':
|
|
95
|
-
case '-h':
|
|
96
|
-
parsed.help = true;
|
|
97
|
-
break;
|
|
98
|
-
case '--defaults':
|
|
99
|
-
parsed.defaults = true;
|
|
100
|
-
break;
|
|
101
|
-
case '--force':
|
|
102
|
-
case '--yes':
|
|
103
|
-
case '-y':
|
|
104
|
-
parsed.force = true;
|
|
105
|
-
break;
|
|
106
|
-
default:
|
|
107
|
-
throw inputError(
|
|
108
|
-
`unknown init option ${arg}`,
|
|
109
|
-
'The init command only accepts --defaults, --force, and --help.',
|
|
110
|
-
'Run `patina init --help` for usage.'
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return parsed;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function promptForConfig(defaults, { target, force, logger = createLogger() }) {
|
|
118
|
-
if (!process.stdin.isTTY) {
|
|
119
|
-
throw inputError(
|
|
120
|
-
'init needs an interactive terminal',
|
|
121
|
-
'guided setup asks questions before writing .patina.yaml.',
|
|
122
|
-
'Run `patina init --defaults` for a non-interactive config.'
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const rl = createInterface({ input, output });
|
|
127
|
-
try {
|
|
128
|
-
if (existsSync(target) && !force) {
|
|
129
|
-
const overwrite = await ask(rl, `Overwrite existing .patina.yaml?`, 'no', ['yes', 'no'], logger);
|
|
130
|
-
if (overwrite !== 'yes') return null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const language = await ask(rl, 'Language', defaults.language, LANGUAGES, logger);
|
|
134
|
-
const profile = await ask(rl, 'Profile', defaults.profile, PROFILES, logger);
|
|
135
|
-
const tone = await ask(rl, 'Tone', defaults.tone, TONES, logger);
|
|
136
|
-
const backendChoices = listBackends().map((b) => b.name);
|
|
137
|
-
const backend = await ask(rl, 'Backend', defaults.backend, backendChoices, logger);
|
|
138
|
-
const maxModels = await askMulti(rl, 'MAX models', defaults.maxModels, ['claude', 'codex', 'gemini'], logger);
|
|
139
|
-
const dispatch = await ask(rl, 'Dispatch mode', defaults.dispatch, DISPATCH_MODES, logger);
|
|
140
|
-
return { language, profile, tone, backend, maxModels, dispatch };
|
|
141
|
-
} finally {
|
|
142
|
-
rl.close();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function ask(rl, label, defaultValue, choices, logger = createLogger()) {
|
|
147
|
-
const choiceHint = choices ? ` (${choices.join('/')})` : '';
|
|
148
|
-
const raw = (await rl.question(`${label}${choiceHint} [${defaultValue}]: `)).trim();
|
|
149
|
-
const value = raw || defaultValue;
|
|
150
|
-
if (choices && !choices.includes(value)) {
|
|
151
|
-
logger.warn('init.unknown_value', { message: `[patina] ${label}: unknown value "${value}", keeping ${defaultValue}` });
|
|
152
|
-
return defaultValue;
|
|
153
|
-
}
|
|
154
|
-
return value;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function askMulti(rl, label, defaultValues, choices, logger = createLogger()) {
|
|
158
|
-
const raw = (await rl.question(`${label} (${choices.join(',')}) [${defaultValues.join(',')}]: `)).trim();
|
|
159
|
-
const values = (raw ? raw.split(',') : defaultValues)
|
|
160
|
-
.map((value) => value.trim())
|
|
161
|
-
.filter(Boolean);
|
|
162
|
-
const valid = values.filter((value) => choices.includes(value));
|
|
163
|
-
if (valid.length === 0) {
|
|
164
|
-
logger.warn('init.no_valid_values', { message: `[patina] ${label}: no valid values, keeping ${defaultValues.join(',')}` });
|
|
165
|
-
return defaultValues;
|
|
166
|
-
}
|
|
167
|
-
return [...new Set(valid)];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function buildInitConfig(answers) {
|
|
171
|
-
return {
|
|
172
|
-
language: answers.language,
|
|
173
|
-
profile: answers.profile,
|
|
174
|
-
tone: answers.tone === 'profile-only' ? null : answers.tone,
|
|
175
|
-
backend: answers.backend,
|
|
176
|
-
'max-models': answers.maxModels,
|
|
177
|
-
dispatch: answers.dispatch,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function commandAvailable(name) {
|
|
182
|
-
try {
|
|
183
|
-
const result = spawnSync(name, ['-V'], { stdio: 'ignore' });
|
|
184
|
-
return result.status === 0;
|
|
185
|
-
} catch {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const MODEL_BY_BACKEND = {
|
|
191
|
-
'codex-cli': 'codex',
|
|
192
|
-
'claude-cli': 'claude',
|
|
193
|
-
'gemini-cli': 'gemini',
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
function printInitHelp() {
|
|
197
|
-
console.log(`patina init — create a project .patina.yaml
|
|
198
|
-
|
|
199
|
-
Usage: patina init [--defaults] [--force]
|
|
200
|
-
|
|
201
|
-
Guided mode asks for language, profile, tone, backend, MAX models, and dispatch
|
|
202
|
-
mode. It preselects authenticated local backends when available.
|
|
203
|
-
|
|
204
|
-
Options:
|
|
205
|
-
--defaults Write detected defaults without prompts
|
|
206
|
-
--force Overwrite an existing .patina.yaml
|
|
207
|
-
`);
|
|
208
|
-
}
|
package/src/manifest.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
// Reproducibility manifest writer — captures enough metadata about a run
|
|
2
|
-
// to reproduce it later (config hash, prompt hash, selected patterns,
|
|
3
|
-
// provider/model, package version, results). Schema is versioned so
|
|
4
|
-
// callers and tooling can detect breaking shape changes.
|
|
5
|
-
|
|
6
|
-
import { createHash } from 'node:crypto';
|
|
7
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
|
-
import { resolve } from 'node:path';
|
|
9
|
-
|
|
10
|
-
export const MANIFEST_SCHEMA_VERSION = '2';
|
|
11
|
-
|
|
12
|
-
export function hashSha256(input) {
|
|
13
|
-
if (input == null) return null;
|
|
14
|
-
const data = typeof input === 'string' ? input : JSON.stringify(input);
|
|
15
|
-
return `sha256:${createHash('sha256').update(data).digest('hex')}`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Build the manifest body. Caller passes the already-resolved run state;
|
|
19
|
-
// this function is pure (no I/O) so it's easy to unit-test.
|
|
20
|
-
export function buildManifest({
|
|
21
|
-
patinaVersion,
|
|
22
|
-
mode,
|
|
23
|
-
lang,
|
|
24
|
-
profile,
|
|
25
|
-
provider,
|
|
26
|
-
backend,
|
|
27
|
-
model,
|
|
28
|
-
configPath,
|
|
29
|
-
config,
|
|
30
|
-
patterns,
|
|
31
|
-
results,
|
|
32
|
-
startedAt,
|
|
33
|
-
finishedAt = new Date().toISOString(),
|
|
34
|
-
temperature = null,
|
|
35
|
-
seed = null,
|
|
36
|
-
}) {
|
|
37
|
-
return {
|
|
38
|
-
manifestVersion: MANIFEST_SCHEMA_VERSION,
|
|
39
|
-
patina: patinaVersion,
|
|
40
|
-
startedAt,
|
|
41
|
-
finishedAt,
|
|
42
|
-
mode,
|
|
43
|
-
lang,
|
|
44
|
-
profile,
|
|
45
|
-
provider: provider ?? null,
|
|
46
|
-
backend: backend ?? null,
|
|
47
|
-
model: model ?? null,
|
|
48
|
-
temperature,
|
|
49
|
-
seed,
|
|
50
|
-
configPath: configPath ?? null,
|
|
51
|
-
configHash: hashSha256(config),
|
|
52
|
-
patterns: (patterns ?? []).map((p) => p.frontmatter?.pack || p.file),
|
|
53
|
-
results: results ?? [],
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Add one input/output pair's hash + ref to the running results array.
|
|
58
|
-
// Mutates the input array for convenience.
|
|
59
|
-
export function appendResult(
|
|
60
|
-
results,
|
|
61
|
-
{
|
|
62
|
-
inputPath,
|
|
63
|
-
prompt,
|
|
64
|
-
outputRef,
|
|
65
|
-
response,
|
|
66
|
-
tokensIn = null,
|
|
67
|
-
tokensOut = null,
|
|
68
|
-
temperature = null,
|
|
69
|
-
seed = null,
|
|
70
|
-
cost = null,
|
|
71
|
-
scores,
|
|
72
|
-
iterationLog,
|
|
73
|
-
calls,
|
|
74
|
-
}
|
|
75
|
-
) {
|
|
76
|
-
const entry = {
|
|
77
|
-
input: inputPath,
|
|
78
|
-
promptHash: hashSha256(prompt),
|
|
79
|
-
responseHash: hashSha256(response),
|
|
80
|
-
output: outputRef,
|
|
81
|
-
tokensIn,
|
|
82
|
-
tokensOut,
|
|
83
|
-
temperature,
|
|
84
|
-
seed,
|
|
85
|
-
cost,
|
|
86
|
-
};
|
|
87
|
-
if (scores) entry.scores = scores;
|
|
88
|
-
if (iterationLog) entry.iterationLog = iterationLog;
|
|
89
|
-
if (calls) entry.calls = calls;
|
|
90
|
-
results.push(entry);
|
|
91
|
-
return results;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function readManifest(path) {
|
|
95
|
-
return normalizeManifest(JSON.parse(readFileSync(path, 'utf8')));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function normalizeManifest(manifest) {
|
|
99
|
-
if (!manifest || typeof manifest !== 'object') {
|
|
100
|
-
throw new Error('Manifest must be a JSON object');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const version = String(manifest.manifestVersion ?? '1');
|
|
104
|
-
if (version === MANIFEST_SCHEMA_VERSION) {
|
|
105
|
-
return {
|
|
106
|
-
...manifest,
|
|
107
|
-
results: normalizeV2Results(manifest.results),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (version === '1') {
|
|
112
|
-
return {
|
|
113
|
-
...manifest,
|
|
114
|
-
manifestVersion: '1',
|
|
115
|
-
temperature: manifest.temperature ?? null,
|
|
116
|
-
seed: manifest.seed ?? null,
|
|
117
|
-
results: normalizeV1Results(manifest.results),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
throw new Error(`Unsupported manifest schema version: ${version}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function normalizeV1Results(results) {
|
|
125
|
-
return (results ?? []).map((entry) => ({
|
|
126
|
-
input: entry.input ?? null,
|
|
127
|
-
promptHash: entry.promptHash ?? null,
|
|
128
|
-
responseHash: entry.responseHash ?? null,
|
|
129
|
-
output: entry.output ?? null,
|
|
130
|
-
tokensIn: entry.tokensIn ?? null,
|
|
131
|
-
tokensOut: entry.tokensOut ?? null,
|
|
132
|
-
temperature: entry.temperature ?? null,
|
|
133
|
-
seed: entry.seed ?? null,
|
|
134
|
-
cost: entry.cost ?? null,
|
|
135
|
-
...(entry.scores ? { scores: entry.scores } : {}),
|
|
136
|
-
...(entry.iterationLog ? { iterationLog: entry.iterationLog } : {}),
|
|
137
|
-
...(entry.calls ? { calls: entry.calls } : {}),
|
|
138
|
-
}));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function normalizeV2Results(results) {
|
|
142
|
-
return (results ?? []).map((entry) => ({
|
|
143
|
-
...entry,
|
|
144
|
-
responseHash: entry.responseHash ?? null,
|
|
145
|
-
tokensIn: entry.tokensIn ?? null,
|
|
146
|
-
tokensOut: entry.tokensOut ?? null,
|
|
147
|
-
temperature: entry.temperature ?? null,
|
|
148
|
-
seed: entry.seed ?? null,
|
|
149
|
-
cost: entry.cost ?? null,
|
|
150
|
-
calls: entry.calls ?? [],
|
|
151
|
-
}));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function writeManifest(dir, manifest, outputs = []) {
|
|
155
|
-
mkdirSync(dir, { recursive: true });
|
|
156
|
-
const manifestPath = resolve(dir, 'manifest.json');
|
|
157
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
158
|
-
for (const { name, content } of outputs) {
|
|
159
|
-
writeFileSync(resolve(dir, name), content);
|
|
160
|
-
}
|
|
161
|
-
return manifestPath;
|
|
162
|
-
}
|
package/src/max-mode.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
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
|
-
}
|