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.
Files changed (193) hide show
  1. package/.patina.default.yaml +29 -29
  2. package/CHANGELOG.md +53 -0
  3. package/NOTICE +21 -0
  4. package/README.md +117 -224
  5. package/README_JA.md +134 -77
  6. package/README_KR.md +132 -74
  7. package/README_ZH.md +137 -80
  8. package/SKILL.md +11 -20
  9. package/artifacts/rebaseline-2025/README.md +147 -0
  10. package/artifacts/rebaseline-2025/human-controls.public.jsonl +250 -0
  11. package/artifacts/rebaseline-2025/intake.example.jsonl +2 -0
  12. package/artifacts/rebaseline-2025/intake.local.example.jsonl +25 -0
  13. package/artifacts/rebaseline-2025/prompts.template.jsonl +7 -0
  14. package/artifacts/rebaseline-2025/sources.ko-public.jsonl +39 -0
  15. package/assets/brand/patina-badge.svg +18 -0
  16. package/assets/brand/patina-mark.svg +8 -0
  17. package/assets/demo/README.md +79 -0
  18. package/core/scoring.md +12 -12
  19. package/core/standalone-prompt.md +3 -1
  20. package/core/stylometry.md +93 -22
  21. package/docs/API.md +1554 -0
  22. package/docs/AUTHENTICATION.md +50 -26
  23. package/docs/AUTHENTICATION_KR.md +54 -29
  24. package/docs/BRANDING.md +9 -8
  25. package/docs/CLI.md +55 -14
  26. package/docs/COOKBOOK.md +8 -21
  27. package/docs/DEMO.md +32 -5
  28. package/docs/EXIT-CODES.md +2 -3
  29. package/docs/FALSE-POSITIVES.md +63 -0
  30. package/docs/FAQ.md +9 -1
  31. package/docs/FAQ_KR.md +3 -1
  32. package/docs/FLAG-PARITY.md +33 -47
  33. package/docs/ISSUE-WAVES.md +57 -0
  34. package/docs/PATTERNS-EN.md +67 -3
  35. package/docs/PATTERNS-JA.md +68 -2
  36. package/docs/PATTERNS-KO.md +70 -7
  37. package/docs/PATTERNS-ZH.md +67 -3
  38. package/docs/PATTERNS.md +5 -5
  39. package/docs/RESEARCH-DOCS-PLATFORM.md +54 -0
  40. package/docs/ROADMAP.md +46 -66
  41. package/docs/TRANSLATIONESE-KO.md +51 -0
  42. package/docs/audits/2026-05-deep-research.md +3 -1
  43. package/docs/benchmarks/README.md +51 -0
  44. package/docs/benchmarks/detector-comparison.json +69 -9
  45. package/docs/benchmarks/detector-comparison.md +10 -5
  46. package/docs/benchmarks/katfish-ko-latest.json +657 -0
  47. package/docs/benchmarks/katfish-ko-latest.md +77 -0
  48. package/docs/benchmarks/latest.json +1183 -108
  49. package/docs/benchmarks/latest.md +84 -60
  50. package/docs/benchmarks/lexicon-freshness-en-2026-05-22.json +1121 -0
  51. package/docs/benchmarks/lexicon-freshness-en-2026-05-22.md +136 -0
  52. package/docs/benchmarks/rebaseline-latest.json +381 -0
  53. package/docs/benchmarks/rebaseline-latest.md +121 -0
  54. package/docs/benchmarks/register-stratified-latest.json +164 -0
  55. package/docs/benchmarks/register-stratified-latest.md +99 -0
  56. package/docs/benchmarks/register-stratified.md +43 -0
  57. package/docs/integrations/github-action.md +44 -11
  58. package/docs/integrations/playground.md +58 -0
  59. package/docs/integrations/pre-commit.md +5 -5
  60. package/docs/integrations/release.md +5 -3
  61. package/docs/integrations/static-sites.md +83 -0
  62. package/docs/research/2025-rebaseline-plan.md +71 -2
  63. package/docs/research/2026-rebaseline.md +102 -0
  64. package/docs/research/adversarial-mps.md +41 -0
  65. package/docs/research/ai-human-metrics.md +35 -23
  66. package/docs/research/human-eval-panel.md +42 -0
  67. package/docs/research/judge-agreement.md +24 -0
  68. package/docs/research/ko-2025-corpus-sources.md +135 -0
  69. package/docs/research/lexicon-freshness-audit.md +64 -0
  70. package/docs/research/zh-ja-lexicon-calibration.md +60 -0
  71. package/docs/social/patina-launch-copy.md +173 -100
  72. package/docs/social/patina-launch-execution.md +94 -0
  73. package/docs/social/patina-launch-korean-first.md +83 -0
  74. package/docs/social/signs-of-ai-writing.md +26 -0
  75. package/docs/social/signs-of-ai-writing_KR.md +26 -0
  76. package/lexicon/ai-en.md +21 -24
  77. package/lexicon/ai-ja.md +158 -0
  78. package/lexicon/ai-ko.md +9 -9
  79. package/lexicon/ai-zh.md +158 -0
  80. package/lexicon/provenance/ai-en.json +970 -0
  81. package/lexicon/provenance/ai-ja.json +542 -0
  82. package/lexicon/provenance/ai-ko.json +866 -0
  83. package/lexicon/provenance/ai-zh.json +542 -0
  84. package/package.json +49 -8
  85. package/patterns/en-communication.md +5 -0
  86. package/patterns/en-content.md +5 -0
  87. package/patterns/en-filler.md +5 -0
  88. package/patterns/en-language.md +29 -1
  89. package/patterns/en-structure.md +5 -0
  90. package/patterns/en-style.md +5 -0
  91. package/patterns/en-viral-hook.md +42 -2
  92. package/patterns/ja-communication.md +5 -0
  93. package/patterns/ja-content.md +5 -0
  94. package/patterns/ja-filler.md +5 -0
  95. package/patterns/ja-language.md +33 -1
  96. package/patterns/ja-structure.md +12 -0
  97. package/patterns/ja-style.md +5 -0
  98. package/patterns/ja-viral-hook.md +41 -2
  99. package/patterns/ko-communication.md +5 -0
  100. package/patterns/ko-content.md +5 -0
  101. package/patterns/ko-filler.md +5 -0
  102. package/patterns/ko-language.md +33 -1
  103. package/patterns/ko-structure.md +25 -6
  104. package/patterns/ko-style.md +5 -0
  105. package/patterns/ko-viral-hook.md +38 -2
  106. package/patterns/zh-communication.md +5 -0
  107. package/patterns/zh-content.md +5 -0
  108. package/patterns/zh-filler.md +5 -0
  109. package/patterns/zh-language.md +37 -1
  110. package/patterns/zh-structure.md +12 -0
  111. package/patterns/zh-style.md +5 -0
  112. package/patterns/zh-viral-hook.md +38 -2
  113. package/playground/README.md +55 -0
  114. package/playground/analytics.js +4 -0
  115. package/playground/analyzer.js +883 -0
  116. package/playground/app.js +157 -0
  117. package/playground/data/lexicons.js +343 -0
  118. package/playground/index.html +138 -0
  119. package/playground/styles.css +267 -0
  120. package/profiles/namuwiki.md +111 -0
  121. package/scripts/adversarial-mps-report.mjs +201 -0
  122. package/scripts/badge-json.mjs +79 -0
  123. package/scripts/benchmark-report.mjs +56 -9
  124. package/scripts/check-release-metadata.mjs +0 -2
  125. package/scripts/detector-comparison.mjs +7 -7
  126. package/scripts/generate-playground-data.mjs +77 -0
  127. package/scripts/katfish-calibration.mjs +464 -0
  128. package/scripts/lexicon-freshness.mjs +485 -0
  129. package/scripts/lint.mjs +1 -1
  130. package/scripts/precommit-score.mjs +4 -3
  131. package/scripts/prose-score.mjs +81 -5
  132. package/scripts/rebaseline-intake.mjs +242 -0
  133. package/scripts/rebaseline-score.mjs +268 -0
  134. package/scripts/rebaseline-summary.mjs +773 -0
  135. package/scripts/rebaseline-web-collect.mjs +410 -0
  136. package/scripts/update-benchmark-ranges.mjs +1 -0
  137. package/src/api.js +69 -105
  138. package/src/auth.js +50 -2
  139. package/src/backends/claude-cli.js +19 -4
  140. package/src/backends/codex-cli.js +19 -3
  141. package/src/backends/contract.js +230 -1
  142. package/src/backends/gemini-cli.js +18 -5
  143. package/src/backends/index.js +87 -12
  144. package/src/backends/kimi-cli.js +161 -0
  145. package/src/cli.js +577 -567
  146. package/src/commands/doctor.js +2 -2
  147. package/src/config.js +29 -0
  148. package/src/errors.js +53 -1
  149. package/src/features/discourse-tells.js +68 -0
  150. package/src/features/index.js +82 -8
  151. package/src/features/lexicon.js +40 -6
  152. package/src/features/markup-leakage.js +69 -0
  153. package/src/features/segment.js +41 -0
  154. package/src/features/signal-strength.js +81 -0
  155. package/src/features/stylometry.js +231 -1
  156. package/src/features/translationese.js +127 -0
  157. package/src/loader.js +76 -0
  158. package/src/logger.js +22 -23
  159. package/src/model-defaults.js +55 -0
  160. package/src/ouroboros.js +31 -0
  161. package/src/output.js +102 -90
  162. package/src/prompt-builder.js +103 -68
  163. package/src/providers.js +51 -4
  164. package/src/scoring.js +210 -2
  165. package/src/security.js +75 -0
  166. package/tests/fixtures/live-quality/en/public-docs-01.md +26 -0
  167. package/tests/fixtures/live-quality/ko/public-docs-01.md +26 -0
  168. package/tests/fixtures/suspect-zones/expected-ranges.json +207 -16
  169. package/tests/fixtures/suspect-zones/ja/ai/ja-ai-04-lexicon.md +11 -0
  170. package/tests/fixtures/suspect-zones/ja/natural/ja-nat-04-lexicon-cold.md +11 -0
  171. package/tests/fixtures/suspect-zones/ko/ai/ko-ai-02.md +4 -5
  172. package/tests/fixtures/suspect-zones/ko/ai/ko-ai-07-ko-diagnostic.md +11 -0
  173. package/tests/fixtures/suspect-zones/zh/ai/zh-ai-04-lexicon.md +11 -0
  174. package/tests/fixtures/suspect-zones/zh/natural/zh-nat-04-lexicon-cold.md +11 -0
  175. package/tests/quality/README.md +188 -11
  176. package/tests/quality/adversarial-mps/fixtures.jsonl +10 -0
  177. package/tests/quality/benchmark.mjs +39 -1
  178. package/tests/quality/dogfood.mjs +5 -3
  179. package/tests/quality/live-fixtures.jsonl +2 -0
  180. package/tests/quality/live-quality.mjs +596 -0
  181. package/tests/quality/ranking-metrics.mjs +136 -0
  182. package/tests/quality/rebaseline-manifest.example.jsonl +5 -0
  183. package/vercel.json +53 -0
  184. package/SKILL-MAX.md +0 -455
  185. package/docs/internal/HARNESS.md +0 -14
  186. package/docs/internal/README.md +0 -14
  187. package/docs/internal/WARP.md +0 -23
  188. package/patina-max/SKILL.md +0 -523
  189. package/patina-max/composite.py +0 -457
  190. package/src/cache.js +0 -106
  191. package/src/commands/init.js +0 -208
  192. package/src/manifest.js +0 -162
  193. package/src/max-mode.js +0 -207
@@ -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
- }