From 61ed3c889b5c83f9030b0c97e6d3ccf1134c5ba3 Mon Sep 17 00:00:00 2001 From: devswha Date: Thu, 30 Jul 2026 09:01:08 +0900 Subject: [PATCH 01/13] =?UTF-8?q?ops:=20measured=20=E2=80=94=20number-bear?= =?UTF-8?q?ing=20documents=20fail=20the=20safety=20gate=20and=20Pro=20pays?= =?UTF-8?q?=20for=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while measuring long-input cost for the character-cap decision; the cost question turned out to be the smaller problem. Ten real pipeline runs on the production prompt: a 146-character number-bearing smoke and a 900-character digit-free document pass on the first attempt, while every probe from 1,270 characters upward terminates in number_safety_failed with numeric_claim_changed, including three coherent single documents at 1,270-1,624 characters. One diagnosed case had the original carrying claim number:1/1 and the rewrite carrying none — the rewriter dropped a number-word and the gate refused, which is the gate working as designed. The cost falls on the customer. rewrite-handler states that the daily and monthly counters increment fail-closed with no refund path, so a failed Pro request still spends one of 100 monthly requests plus its characters against the 50,000 total and returns an error, while both rewrite attempts bill us about $0.09. A customer drafting number-bearing text can pay for a month and receive nothing, against an account Polar holds to a 0.4% chargeback rate — and the card offers 'up to 20,000 characters each'. Records the confounds rather than hiding them: the digit-free probes are concatenated fragments whose topic jumps may provoke compression, the coherent documents are patina's own unusually number-dense examples, and every row is n=1 for its size, so nothing here establishes a rate. The four available responses each trade away something real — refund policy vs abuse, retry budget vs margin, advertised ceiling vs Pro's reason to exist, or changing the rewrite prompt — so the choice is left open rather than made unilaterally. --- .../number-safety-failure-rate-20260730.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/operations/number-safety-failure-rate-20260730.md diff --git a/docs/operations/number-safety-failure-rate-20260730.md b/docs/operations/number-safety-failure-rate-20260730.md new file mode 100644 index 0000000..34b4a98 --- /dev/null +++ b/docs/operations/number-safety-failure-rate-20260730.md @@ -0,0 +1,83 @@ +# Measured: number-bearing documents fail the safety gate, and Pro pays for it + +Found while measuring long-input cost for the Pro character-cap decision. The +cost question is secondary to what the measurement exposed. + +## What was measured + +Real pipeline runs (`runWebRewriteStream`, `tier: 'pro'`, gemini-3.6-flash, +production prompt), one request per row: + +| probe | chars | digits | terminal | rewrite attempts | +|---|---:|---|---|---:| +| production smoke (`14:30`, `23,000`) | 146 | yes | **done** | 1 | +| digit-free fixture prose | 900 | no | **done** | 1 | +| digit-free fixture prose | 2,301 | no | `number_safety_failed` | 2 | +| digit-free fixture prose | 2,693 | no | `number_safety_failed` | 2 | +| digit-free fixture prose | 2,806 | no | done — **rescued by the retry** | 2 | +| digit-free fixture prose | 7,560 | no | `number_safety_failed` | 2 | +| digit-free fixture prose | 19,540 | no | `number_safety_failed` | 2 | +| `round3-gemini-3-casual-full.md` | 1,624 | yes | `number_safety_failed` | 2 | +| `round3-claude-casual-full.md` | 1,489 | yes | `number_safety_failed` | 2 | +| `sample-rewritten-claude.md` | 1,270 | yes | `number_safety_failed` | 2 | + +Every failure reports `numeric_claim_changed` from `numeric-safety-v2`. In one +diagnosed case the original carried the claim `number:1/1` and the rewrite +carried none — the rewriter dropped a number-word and the gate refused, which is +the gate working exactly as designed. + +## The part that costs money + +`src/rewrite-handler.js` states it directly: the daily and monthly counters +"increment fail-closed with no refund path". So a Pro request that terminates in +`number_safety_failed` still consumes one of the customer's 100 monthly requests +and its characters against the 50,000 monthly total — and returns an error +instead of a rewrite. Both rewrite attempts are billed to us as well, roughly +$0.09 for a 2,700-character document. + +A customer working on number-bearing drafts can therefore spend a paid month +receiving nothing. That is a refund request at best and a chargeback at worst, +against an account Polar holds to a 0.4% chargeback rate. + +Meanwhile the pricing card offers "Up to 20,000 characters each". + +## Honest limits of this evidence + +Each probe family carries a confound, and none of them is a real customer: + +- The digit-free probes are unrelated paragraphs concatenated to a target + length. The topic jumps may themselves provoke compression. +- The coherent documents are patina's own before/after examples, which are + unusually **number-dense** (scores, percentages, version strings). A typical + customer paragraph is not. +- Every row is n=1 for its size. Nothing here establishes a rate. + +What the rows do support: number-bearing text fails well below the advertised +ceiling, short number-bearing text passes (twice, including in production), and +the single retry rescues some failures but not most. + +## Reproduction + +Runs used `runWebRewriteStream` directly with `GEMINI_API_KEY`, reading +`result.numberSafety` and `result.attempts.rewrite[].usage`. No production +traffic and no customer data involved. + +## The decision this forces + +Not taken here, because each option trades away something real: + +1. **Stop charging quota on `number_safety_failed`.** Correct from the + customer's side — our gate rejected our own rewrite. But the no-refund rule + exists to stop crafted always-failing input from buying unlimited free LLM + spend, so a refund needs its own bound. +2. **Raise the retry budget.** One retry already rescues some cases. Each extra + attempt is real money against a ~55% margin. +3. **Lower the advertised character ceiling.** Honest, but Pro would advertise + less than the free tier's 4,000 characters, which removes its reason to exist. +4. **Reduce the rewriter's numeral drift** so the gate stops firing. The real + fix, and the only one that improves the product rather than reallocating the + loss. It touches the rewrite prompt, which is out of scope without an + explicit ask. + +The measurement should be repeated on coherent customer-shaped prose with +ordinary numeric density before any of these is chosen. From 699c91bb680d1c24b596989c746a77d792e9d354 Mon Sep 17 00:00:00 2001 From: devswha Date: Thu, 30 Jul 2026 09:03:46 +0900 Subject: [PATCH 02/13] =?UTF-8?q?ops:=20correct=20the=20cost=20figures=20?= =?UTF-8?q?=E2=80=94=20the=20first=20measurement=20bypassed=20the=20reason?= =?UTF-8?q?ing=20cut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scoring reasoning cut is scoped to request.provider === 'gemini'. The first pass of this measurement never set that field, so it silently served the pre-cut path and inflated the scorers. Isolated on the same 518-character input: scorer thinking 923/497 and $0.0573 with the field unset, versus 0/0 and $0.0311 with it set. Two corrections follow. The production-path cost of a 518-character English request is $0.0311, about $3.11 of COGS for a full 100-request month against $8.49 net revenue — roughly 63% margin, better than the ~55% on record rather than worse. An earlier draft read $0.05-0.058 as a margin problem; it was measuring a path production does not use. Also drops an unmeasured number: the note attributed 'roughly $0.09' to a 2,700-character document, which was the 19,540-character probe's figure transposed onto a size never priced. Replaced with the measured $0.069-0.089 range and scoped to the probe it came from. The number-safety findings are unaffected: failures occur at the rewrite stage before scoring, and the rewrite call is excluded from the cut by design. --- .../number-safety-failure-rate-20260730.md | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/operations/number-safety-failure-rate-20260730.md b/docs/operations/number-safety-failure-rate-20260730.md index 34b4a98..9ba84c6 100644 --- a/docs/operations/number-safety-failure-rate-20260730.md +++ b/docs/operations/number-safety-failure-rate-20260730.md @@ -32,8 +32,28 @@ the gate working exactly as designed. "increment fail-closed with no refund path". So a Pro request that terminates in `number_safety_failed` still consumes one of the customer's 100 monthly requests and its characters against the 50,000 monthly total — and returns an error -instead of a rewrite. Both rewrite attempts are billed to us as well, roughly -$0.09 for a 2,700-character document. +instead of a rewrite. Both rewrite attempts are billed to us as well: measured +at **$0.069–$0.089** across two runs of the 19,540-character probe. The failed +path never reaches scoring, so that is rewrite cost alone. + +### A measurement trap worth naming + +The scoring reasoning cut is scoped to `request.provider === 'gemini'`. Omitting +that field — as the first pass of this measurement did — silently serves the +**pre-cut** cost and inflates the scorers. Isolated on the same 518-character +input: + +| `request.provider` | scorer thinking (MPS / fidelity) | full pipeline | +|---|---|---:| +| unset | 923 / 497 | $0.0573 | +| `'gemini'` | **0 / 0** | **$0.0311** | + +So the cut behaves exactly as documented, and the production-path cost of a +518-character English request is **$0.0311** — about $3.11 of COGS for a full +100-request month against $8.49 net revenue, roughly **63% margin**. That is +better than the ~55% on record, which stands as the conservative figure. An +earlier draft of this note reported $0.05–$0.058 per request and read that as a +margin problem; it was measuring the pre-cut path. A customer working on number-bearing drafts can therefore spend a paid month receiving nothing. That is a refund request at best and a chargeback at worst, From fc39d1fe8b20ed747d849428e1d961a9317ce8a5 Mon Sep 17 00:00:00 2001 From: devswha Date: Thu, 30 Jul 2026 09:09:45 +0900 Subject: [PATCH 03/13] =?UTF-8?q?ops:=20retract=20the=20number-safety=20al?= =?UTF-8?q?arm=20=E2=80=94=20the=20failures=20were=20a=20probe=20artifact?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six production runs on customer-shaped drafts, written as a product-update post with ordinary numeric density, all terminate in done with every numeral preserved: 1,190 characters with 6 numerals three times, and 2,400 characters with 14 numerals three times. The gate does not fire on this material at either length. The earlier note claimed number-bearing documents fail the gate and framed it as live customer harm. Both failing probe families were unlike customer prose: patina's own examples/*.md are before/after documents dense with scores, percentages and version strings, and the digit-free probes were unrelated paragraphs concatenated to hit a length target, where topic jumps invite the compression that drops a claim. Two families failing looked like corroboration when they only shared the property of being unrepresentative, and the obvious control was listed as future work instead of being run first. Retraction kept in place rather than deleted, with the raw failing rows retained, so the reasoning error stays visible. Withdraws the recommendation to reduce the rewriter's numeral drift: on customer-shaped input there is no measured drift to reduce, and changing the rewrite prompt on this basis would be acting against an artifact. What survives: rewrite-handler charges the monthly counters with no refund path, so when the gate does fire a Pro customer loses quota and receives an error. Wrong on its own terms, but a correctness issue rather than an emergency at this trigger rate, and a refund path still needs a bound against crafted always-failing input. --- .../number-safety-failure-rate-20260730.md | 147 +++++++++--------- 1 file changed, 72 insertions(+), 75 deletions(-) diff --git a/docs/operations/number-safety-failure-rate-20260730.md b/docs/operations/number-safety-failure-rate-20260730.md index 9ba84c6..aa13868 100644 --- a/docs/operations/number-safety-failure-rate-20260730.md +++ b/docs/operations/number-safety-failure-rate-20260730.md @@ -1,103 +1,100 @@ -# Measured: number-bearing documents fail the safety gate, and Pro pays for it +# Number-safety failures: measured, then found to be a probe artifact -Found while measuring long-input cost for the Pro character-cap decision. The -cost question is secondary to what the measurement exposed. +> **Corrected.** An earlier version of this note was titled "number-bearing +> documents fail the safety gate, and Pro pays for it" and treated the gate as a +> live customer-harm path. Follow-up measurement on customer-shaped drafts does +> not support that. The failures were real but the probes were not +> representative. The retraction is below, kept in place rather than deleted so +> the reasoning error stays visible. -## What was measured +## What actually happens on customer-shaped input -Real pipeline runs (`runWebRewriteStream`, `tier: 'pro'`, gemini-3.6-flash, -production prompt), one request per row: +Production runs against `https://patina.vibetip.help/api/rewrite`, free tier, +drafts written as a product-update post with ordinary numeric density: + +| draft | chars | numerals | runs | terminal | numerals dropped | +|---|---:|---:|---:|---|---| +| product update, short | 1,190 | 6 | 3 | **done** ×3 | **none** | +| product update, long | 2,400 | 14 | 3 | **done** ×3 | **none** | + +Six for six, with every numeral preserved in the output. The gate does not fire +on this material at either length. + +## What the failing probes actually were + +Every failure reproduced only on input that no customer would paste: + +| probe family | why it is not representative | +|---|---| +| `examples/*.md` (1,270–1,624 chars) | patina's own before/after documents — dense with scores, percentages, version strings, and table data. Numeric density far above prose. | +| concatenated fixture paragraphs (2,301–19,540 chars) | unrelated paragraphs joined to hit a length target. The topic jumps invite compression, and compression is what drops a claim. | + +Raw results retained for the record: | probe | chars | digits | terminal | rewrite attempts | |---|---:|---|---|---:| -| production smoke (`14:30`, `23,000`) | 146 | yes | **done** | 1 | -| digit-free fixture prose | 900 | no | **done** | 1 | +| digit-free fixture prose | 900 | no | done | 1 | | digit-free fixture prose | 2,301 | no | `number_safety_failed` | 2 | | digit-free fixture prose | 2,693 | no | `number_safety_failed` | 2 | -| digit-free fixture prose | 2,806 | no | done — **rescued by the retry** | 2 | +| digit-free fixture prose | 2,806 | no | done — rescued by the retry | 2 | | digit-free fixture prose | 7,560 | no | `number_safety_failed` | 2 | | digit-free fixture prose | 19,540 | no | `number_safety_failed` | 2 | | `round3-gemini-3-casual-full.md` | 1,624 | yes | `number_safety_failed` | 2 | | `round3-claude-casual-full.md` | 1,489 | yes | `number_safety_failed` | 2 | | `sample-rewritten-claude.md` | 1,270 | yes | `number_safety_failed` | 2 | -Every failure reports `numeric_claim_changed` from `numeric-safety-v2`. In one -diagnosed case the original carried the claim `number:1/1` and the rewrite -carried none — the rewriter dropped a number-word and the gate refused, which is -the gate working exactly as designed. +Every failure reported `numeric_claim_changed` from `numeric-safety-v2`. One +diagnosed case had the original carrying claim `number:1/1` and the rewrite +carrying none — the gate refusing a dropped number-word, which is the gate +working as designed. -## The part that costs money +## The reasoning error -`src/rewrite-handler.js` states it directly: the daily and monthly counters -"increment fail-closed with no refund path". So a Pro request that terminates in -`number_safety_failed` still consumes one of the customer's 100 monthly requests -and its characters against the 50,000 monthly total — and returns an error -instead of a rewrite. Both rewrite attempts are billed to us as well: measured -at **$0.069–$0.089** across two runs of the 19,540-character probe. The failed -path never reaches scoring, so that is rewrite cost alone. +Two probe families both failed, which looked like corroboration. It was not: +they share the property of being unlike customer prose, and neither was tested +against the alternative before the alarm was written. The correct move — testing +a customer-shaped draft — was named in the first version as future work and +should have been the first step instead. -### A measurement trap worth naming +## What remains true and worth fixing -The scoring reasoning cut is scoped to `request.provider === 'gemini'`. Omitting -that field — as the first pass of this measurement did — silently serves the -**pre-cut** cost and inflates the scorers. Isolated on the same 518-character -input: +`src/rewrite-handler.js` charges the daily and monthly counters with, in its own +words, "no refund path". So when the gate does fire on a Pro request, the +customer still spends one of 100 monthly requests plus its characters against +the 50,000 total, and receives an error. That is wrong on its own terms — our +gate rejected our own rewrite — independently of how often it happens. -| `request.provider` | scorer thinking (MPS / fidelity) | full pipeline | -|---|---|---:| -| unset | 923 / 497 | $0.0573 | -| `'gemini'` | **0 / 0** | **$0.0311** | +On this evidence it is a correctness issue rather than an emergency: the trigger +rate on realistic input is low enough that six consecutive production runs never +hit it. A refund path still needs a bound, because the rule exists to stop +crafted always-failing input from buying unlimited free inference. -So the cut behaves exactly as documented, and the production-path cost of a -518-character English request is **$0.0311** — about $3.11 of COGS for a full -100-request month against $8.49 net revenue, roughly **63% margin**. That is -better than the ~55% on record, which stands as the conservative figure. An -earlier draft of this note reported $0.05–$0.058 per request and read that as a -margin problem; it was measuring the pre-cut path. +## What this evidence does **not** justify -A customer working on number-bearing drafts can therefore spend a paid month -receiving nothing. That is a refund request at best and a chargeback at worst, -against an account Polar holds to a 0.4% chargeback rate. +Changing the rewrite prompt. The earlier note named reducing the rewriter's +numeral drift as "the real fix"; on customer-shaped input there is no measured +drift to reduce. Touching the rewrite prompt on this basis would be a change +made against an artifact. -Meanwhile the pricing card offers "Up to 20,000 characters each". +## Cost, measured on the production path -## Honest limits of this evidence +A trap worth naming: the scoring reasoning cut is scoped to +`request.provider === 'gemini'`, and omitting that field silently serves the +pre-cut cost. Isolated on the same 518-character input: -Each probe family carries a confound, and none of them is a real customer: - -- The digit-free probes are unrelated paragraphs concatenated to a target - length. The topic jumps may themselves provoke compression. -- The coherent documents are patina's own before/after examples, which are - unusually **number-dense** (scores, percentages, version strings). A typical - customer paragraph is not. -- Every row is n=1 for its size. Nothing here establishes a rate. +| `request.provider` | scorer thinking (MPS / fidelity) | full pipeline | +|---|---|---:| +| unset | 923 / 497 | $0.0573 | +| `'gemini'` | **0 / 0** | **$0.0311** | -What the rows do support: number-bearing text fails well below the advertised -ceiling, short number-bearing text passes (twice, including in production), and -the single retry rescues some failures but not most. +So the production-path cost of a 518-character English request is **$0.0311** — +about $3.11 of COGS for a full 100-request month against $8.49 net revenue, +roughly **63% margin**, better than the ~55% on record. A failed request costs +rewrite attempts only, measured at $0.069–$0.089 on the 19,540-character probe. ## Reproduction -Runs used `runWebRewriteStream` directly with `GEMINI_API_KEY`, reading -`result.numberSafety` and `result.attempts.rewrite[].usage`. No production -traffic and no customer data involved. - -## The decision this forces - -Not taken here, because each option trades away something real: - -1. **Stop charging quota on `number_safety_failed`.** Correct from the - customer's side — our gate rejected our own rewrite. But the no-refund rule - exists to stop crafted always-failing input from buying unlimited free LLM - spend, so a refund needs its own bound. -2. **Raise the retry budget.** One retry already rescues some cases. Each extra - attempt is real money against a ~55% margin. -3. **Lower the advertised character ceiling.** Honest, but Pro would advertise - less than the free tier's 4,000 characters, which removes its reason to exist. -4. **Reduce the rewriter's numeral drift** so the gate stops firing. The real - fix, and the only one that improves the product rather than reallocating the - loss. It touches the rewrite prompt, which is out of scope without an - explicit ask. - -The measurement should be repeated on coherent customer-shaped prose with -ordinary numeric density before any of these is chosen. +Local probes used `runWebRewriteStream` directly with `GEMINI_API_KEY`, reading +`result.numberSafety` and `result.attempts.rewrite[].usage`. The +customer-shaped drafts ran against production on the free tier. No customer data +was involved. From 9f4e6c9273b3548dadb165948fe97dcd175a07fb Mon Sep 17 00:00:00 2001 From: devswha Date: Thu, 30 Jul 2026 09:12:46 +0900 Subject: [PATCH 04/13] ops: establish that the quota machinery actually enforces on production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Pro monthly cap could not be exercised directly — that needs a production license, and the verification key was shredded after use while a confirmed checkout session does not re-issue a customer token. Rather than leaving the whole claim unevidenced, the layer underneath it was tested: the free tier's hourly burst limit fired on production with 429 hourly burst exceeded after ten requests in the window. That matters because the Pro monthly cap runs through the same limiter, the same KV, and the same HMAC-keyed bucket, differing only in key and window. So the counters demonstrably increment and refuse in the deployed environment, which was the open question. The 100-request threshold itself remains proven only against a local handler, and the note now says exactly that rather than listing the cap as wholly unverified. --- docs/operations/polar-binding-migration.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/operations/polar-binding-migration.md b/docs/operations/polar-binding-migration.md index 8c923d9..1a34503 100644 --- a/docs/operations/polar-binding-migration.md +++ b/docs/operations/polar-binding-migration.md @@ -44,9 +44,18 @@ Polar gate was live — it was the Lemon Squeezy validator declining a key it ha never issued. Shipped as v6.3.3. Still unproven: any card-backed purchase, payout (the review governs money -reaching the owner, not the ability to sell), refund and cancellation-revocation -behaviour on production, and the monthly 100-request cap against this deployment -rather than a local handler. +reaching the owner, not the ability to sell), and refund and +cancellation-revocation behaviour on production. + +The Pro monthly request cap is **partially** established. It cannot be exercised +directly without a production license, and the verification license was shredded +after use while a confirmed checkout session does not re-issue a customer token. +What was verified instead is the machinery underneath it: the free tier's hourly +burst limit fired on production with `429 hourly burst exceeded` after ten +requests in the window. The monthly Pro cap runs through the same limiter, the +same KV, and the same HMAC-keyed bucket — only the key and window differ — so the +counters demonstrably increment and block in the deployed environment. The +100-request threshold itself is still only proven against a local handler. ### Payment readiness: blocked, then opened From 9aebbfc766924684be25c8f0de0b1bc7b5c85e13 Mon Sep 17 00:00:00 2001 From: devswha Date: Fri, 31 Jul 2026 03:09:19 +0900 Subject: [PATCH 05/13] feat!: remove ouroboros mode Retain the iterative quality baseline under a neutral research-only name, migrate scoring and verification config ownership, default the playground to English, and synchronize release metadata to 7.0.0. --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .cursor/rules/patina.md | 4 +- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .patina.default.yaml | 22 +- CHANGELOG.md | 38 +++ README.md | 10 +- README_JA.md | 10 +- README_KR.md | 10 +- README_ZH.md | 10 +- SKILL.md | 79 +----- SUPPORT.md | 2 +- agents/patina-fidelity-auditor.md | 8 +- core/scoring.md | 78 ++---- core/standalone-prompt.md | 40 +-- docs/API.md | 249 +++++++++++++----- docs/ARCHITECTURE.md | 12 +- docs/CLI.md | 14 +- docs/COOKBOOK.md | 23 +- docs/EXIT-CODES.md | 4 +- docs/FAQ.md | 6 +- docs/FAQ_KR.md | 6 +- docs/FLAG-PARITY.md | 4 +- docs/GLOSSARY.md | 7 - docs/HARNESS.md | 4 +- docs/PATTERNS.md | 2 +- docs/ROADMAP.md | 18 +- docs/agents.md | 6 +- docs/research/adversarial-mps.md | 2 +- docs/social/patina-launch-korean-first.md | 2 +- package-lock.json | 4 +- package.json | 6 +- packages/patina-humanizer/package.json | 4 +- playground/chatgpt.js | 34 +-- playground/index.html | 4 +- playground/src/web-rewrite-contract.js | 4 +- scripts/adversarial-mps-report.mjs | 2 +- scripts/check-retired-concepts.mjs | 222 ++++++++++++++++ scripts/check-v6.4-preflight-hold.mjs | 32 ++- scripts/generate-api-docs.mjs | 1 - .../iterative-rewrite-baseline.mjs | 46 ++-- scripts/rewrite-ab.mjs | 54 ++-- src/api.js | 2 +- src/cli/args.js | 9 - src/cli/run.js | 2 +- src/prompt-builder.js | 14 +- src/scoring.js | 14 +- src/verify.js | 6 +- src/web-rewrite-contract.js | 4 +- tests/e2e/cli-persona.test.js | 42 +++ tests/quality/README.md | 20 +- tests/unit/check-release-metadata.test.js | 2 +- tests/unit/config.test.js | 56 ++++ ....js => iterative-rewrite-baseline.test.js} | 56 ++-- tests/unit/loader.test.js | 5 +- tests/unit/persona-args.test.js | 5 +- tests/unit/playground-a11y.test.js | 5 + tests/unit/prompt-builder.snapshot.test.js | 19 +- tests/unit/retired-concepts.test.js | 132 ++++++++++ tests/unit/rewrite-ab.test.js | 38 +-- tests/unit/scoring.test.js | 13 +- tests/unit/threshold-parity.test.js | 26 +- tests/unit/verify.test.js | 52 +++- 63 files changed, 1097 insertions(+), 514 deletions(-) create mode 100644 scripts/check-retired-concepts.mjs rename src/ouroboros.js => scripts/iterative-rewrite-baseline.mjs (83%) rename tests/unit/{ouroboros.test.js => iterative-rewrite-baseline.test.js} (81%) create mode 100644 tests/unit/retired-concepts.test.js diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7000e6b..8506488 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ { "name": "patina", "source": "./", - "version": "6.3.4", + "version": "7.0.0", "description": "Strip the AI packaging, keep the meaning. Detect and rewrite AI writing patterns across KO/EN/ZH/JA with MPS/fidelity checks. Runs the root SKILL.md as the /patina skill.", "homepage": "https://github.com/devswha/patina", "repository": "https://github.com/devswha/patina", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 41f3179..c6eed3b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://www.schemastore.org/claude-code-plugin-manifest.json", "name": "patina", - "version": "6.3.4", + "version": "7.0.0", "description": "Detect and rewrite AI writing patterns in Korean, English, Chinese, and Japanese so text reads as if a human wrote it. Meaning-preservation (MPS) verified, audit-friendly. The root SKILL.md is loaded as the /patina skill.", "author": { "name": "devswha", diff --git a/.cursor/rules/patina.md b/.cursor/rules/patina.md index 0dcee20..565a816 100644 --- a/.cursor/rules/patina.md +++ b/.cursor/rules/patina.md @@ -52,7 +52,7 @@ The project uses a **plugin architecture**: patterns are plugins (`patterns/{lan en: 8: amplify ``` -4. If the profile needs custom AI/fidelity balance, add to `.patina.default.yaml` under `ouroboros.combined-weights` +4. If the profile needs custom AI/fidelity balance, add to `.patina.default.yaml` under `scoring.combined-weights` 5. Update `README.md` profile table ## When Modifying SKILL.md @@ -97,7 +97,7 @@ The skill supports these modes (defined in `.patina.default.yaml`): - `diff` — shows what changed and why - `audit` — detects patterns only - `score` — AI-likeness score 0-100 -- `ouroboros` — iterative self-improvement loop +- `strict` — skill-only multi-pass verification flow (`/patina --strict`), not a runtime output mode ## Important Constraints diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1d7b342..0553ef1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -25,7 +25,7 @@ body: id: mode attributes: label: Mode - options: [rewrite, audit, score, diff, ouroboros, max, install, other] + options: [rewrite, audit, score, diff, max, install, other] validations: required: true - type: textarea diff --git a/.patina.default.yaml b/.patina.default.yaml index 1fc9cd7..b746395 100644 --- a/.patina.default.yaml +++ b/.patina.default.yaml @@ -1,4 +1,4 @@ -version: "6.3.4" +version: "7.0.0" language: ko # Korean (default) -- auto-loads all ko-*.md patterns # language: en -- English -- auto-loads all en-*.md patterns # language: zh -- Chinese -- auto-loads all zh-*.md patterns @@ -7,11 +7,6 @@ language: ko # Korean (default) -- auto-loads all ko-*.md patterns profile: default output: rewrite # rewrite | diff | audit | score -# Opt-in: request OpenAI-compatible structured output (response_format -# json_object) for ouroboros LLM scoring. Default false; only the openai-http -# backend forwards it (never sent to local CLI backends). Some OpenAI-compatible -# endpoints reject response_format — leave false unless your endpoint supports it. -structured-output: false # Tone categorization (v3.10): register axis (casual/professional) for ko/en. # Resolution: explicit --tone > config tone > config profile. @@ -104,15 +99,6 @@ scoring: enabled: true divergence-threshold: 20 # warn and prefer the more pessimistic score above this delta combined-weight: 0 # default off for combinedScore; users may opt in (e.g. 0.2) - -# Ouroboros: iterative self-improvement loop -# Runs the full humanization pipeline repeatedly until the AI score converges. -# Enable via config or CLI flag: --ouroboros -ouroboros: - enabled: false # opt-in; --ouroboros CLI flag overrides to true - target-score: 30 # stop when overall score <= this value (0-100) - max-iterations: 3 # hard limit on loop iterations - plateau-threshold: 10 # stop if score improves by less than this between iterations category-weights: ko: content: 0.18 @@ -146,8 +132,6 @@ ouroboros: filler: 0.08 structure: 0.15 viral-hook: 0.10 # score-only pack (excluded from rewrite) - fidelity-floor: 70 # stop ouroboros if fidelity drops below this; also the --verify fidelity floor - mps-floor: 70 # stop ouroboros if MPS (meaning preservation) drops below this; also the --verify MPS floor combined-weights: default: ai-likeness: 0.60 @@ -184,6 +168,10 @@ ouroboros: medium: 2 low: 1 +verification: + mps-floor: 70 + fidelity-floor: 70 + # Stylometry: deterministic statistical preprocessing (SKILL.md Step 4.6) # Calculates burstiness (sentence-length CV) and MATTR (lexical diversity) per # paragraph, marks suspect zones, and feeds them into 5a/5b as internal memory. diff --git a/CHANGELOG.md b/CHANGELOG.md index 820fd2c..8aef5d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,44 @@ All notable changes to patina. Dates are release dates (YYYY-MM-DD). Semver rationale: patch | minor | major — explain whether this changes patterns, schemas, CLI behavior, or docs only. ``` +## 7.0.0 — 2026-07-30 + +**Removes the retired iterative rewrite product contract, gives scoring and verification settings neutral ownership, and preserves the comparator only as unsupported research.** + +Semver rationale: major — removes public configuration keys and the obsolete agent-skill mode without compatibility aliases. Existing custom configurations must migrate the shared score and verification settings below. + +### Removed + +- The retired iterative mode is gone from the agent skill, CLI migration tombstone, default configuration, help/support surfaces, generated API, and current documentation. Node users keep `--verify`; agent-skill users keep the independent `/patina --strict` flow. +- Loop-only `enabled`, `target-score`, `max-iterations`, and `plateau-threshold` settings are no longer product configuration. +- The unused top-level `structured-output` setting is deleted. The scorer's explicit runtime `responseFormat` API remains available to supported callers. + +### Changed (breaking) + +- Move `category-weights`, `combined-weights`, and `severity-points` from the former top-level loop-settings block to `scoring`. +- Move the shared `mps-floor` and `fidelity-floor` values to `verification`. Their defaults remain 70/70. +- Keep `personas.thresholds.mps_floor` and `personas.thresholds.fidelity_floor` separate; persona gate overrides do not affect `--verify`, and verification overrides do not affect persona gates. +- Rename the opt-in A/B arm to `iterative-baseline` and move its runner under `scripts/`. It remains packaged for `npm run quality:rewrite-ab`, but is unsupported research and is absent from CLI/help/config, hosted APIs, and generated public API docs. +- Persona, profile, and tone behavior is unchanged. Persona remains the reusable voice axis, tone remains the KO/EN register override, and profile-policy consolidation remains follow-up work. + +### Migration + +```yaml +scoring: + category-weights: { ... } + combined-weights: { ... } + severity-points: + high: 3 + medium: 2 + low: 1 + +verification: + mps-floor: 70 + fidelity-floor: 70 +``` + +No legacy alias is read. Move custom values before upgrading. + ## 6.3.4 — 2026-07-29 **Corrects the Pro pricing card, which advertised a burst guard as an entitlement and contradicted the real monthly cap.** diff --git a/README.md b/README.md index 1b5cc7b..1e924c9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ License: MIT Skill: Claude Code | Codex | Cursor | OpenCode Languages: KO | EN | ZH | JA - Version 6.3.4 + Version 7.0.0

@@ -110,7 +110,7 @@ For large `--batch` runs, prefer an OpenAI-compatible HTTP backend; local CLI ba | **184 patterns** | 37 rewrite-capable + 9 score-only viral-hook per language (46 each across KO/EN/ZH/JA) — see the full 184-pattern catalog in [PATTERNS.md](docs/PATTERNS.md) | | **Modes** | rewrite · verify · audit · score · diff | | **Surfaces** | agent skill · Node CLI · in-place preview · browser playground (rewrite + score) | -| **Voice** | `--persona` (built-in + your own, ko/en/zh/ja) is the sole voice axis · `--tone` register · `--profile` pattern policy — composable with a fixed precedence | +| **Voice controls** | `--persona` is the reusable voice axis · `--tone` overrides register · `--profile` sets Node pattern policy; some skill/core paths still carry legacy profile voice guidance | | **Free usage** | logged-in `codex`, `claude`, or `gemini` CLI can run rewrites without `PATINA_API_KEY` | | **Calibration** | 67.3% editing-hotspot catch [63.5–71.0%] across GPT-5.5 / Claude Sonnet 4.6 / Gemini 2.5 Pro (n=600, KO+EN); 16.0% false positives [11.6–21.7%] on KO+EN human controls (n=200) | | **License** | MIT | @@ -158,7 +158,7 @@ patina persona edit my-voice --name "Founder voice" # copy-on-edit into patina persona rm my-voice # remove a custom persona (--force to skip the confirm) ``` -Works on ko/en/zh/ja and composes with `--tone` (register) and `--profile` (pattern policy). The persona is the sole voice owner; register precedence is `--tone` > persona. A persona shapes voice but never lowers the meaning floors — authored personas are validated on save, and the safety gate still enforces MPS/fidelity + dropped-number checks. +Works on ko/en/zh/ja and composes with `--tone` and `--profile`. In Node, persona owns reusable voice, register precedence is `--tone` > persona, and profile controls pattern policy. Some skill/core-prompt paths still contain legacy profile voice guidance, so that separation is not universal yet. None of these controls lowers the meaning floors: authored personas are validated on save, and rewrites still enforce MPS/fidelity and dropped-number checks. ## CI @@ -204,11 +204,11 @@ If meaning drifts, the change is retried or rolled back. Deterministic analysis ```yaml # .patina.default.yaml -version: "6.3.4" +version: "7.0.0" language: ko # ko | en | zh | ja profile: default output: rewrite # rewrite | diff | audit | score -tone: # casual | professional | auto (register; genre = profile) +tone: # casual | professional | auto (register override; profile = pattern policy) ``` Project `.patina.yaml` overrides defaults. Pattern packs are auto-discovered by language prefix. Additive list keys (`blocklist`, `allowlist`, `skip-patterns`) merge; other arrays replace. diff --git a/README_JA.md b/README_JA.md index 1730bca..496f842 100644 --- a/README_JA.md +++ b/README_JA.md @@ -6,7 +6,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Skill](https://img.shields.io/badge/Skill-Claude%20Code%20%7C%20Codex%20%7C%20Cursor%20%7C%20OpenCode-blueviolet)](#クイックスタート) [![Multi-language](https://img.shields.io/badge/Languages-KO%20%7C%20EN%20%7C%20ZH%20%7C%20JA-green)](https://github.com/devswha/patina) -[![Version](https://img.shields.io/badge/version-6.3.4-blue)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-7.0.0-blue)](CHANGELOG.md) > **AIっぽさだけを落として、意味はそのまま。** @@ -99,7 +99,7 @@ printf '%s\n' 'Coffee has emerged as a pivotal cultural phenomenon.' \ | **184 パターン** | 各言語 37 個の書き換え可能パターン + 9 個のスコア専用 viral-hook(KO/EN/ZH/JA 各 46 個) — 完全な 184 パターンカタログは [PATTERNS.md](docs/PATTERNS.md) を参照 | | **モード** | rewrite · verify · audit · score · diff | | **利用形態** | agent skill · Node CLI · ページ内 preview · ブラウザ playground(rewrite + score) | -| **ボイス** | `--persona`(組み込み + 自作、ko/en/zh/ja)· `--tone` レジスター · `--profile` ジャンル — 固定の優先順位で組み合わせ可能 | +| **ボイス制御** | `--persona` は再利用ボイス · `--tone` はレジスター上書き · `--profile` は Node のパターン方針(一部 skill/core 経路には旧 profile ボイス指示が残る) | | **無料利用** | ログイン済みの `codex`、`claude`、`gemini` CLI なら `PATINA_API_KEY` なしで書き換え可能 | | **キャリブレーション** | GPT-5.5 / Claude Sonnet 4.6 / Gemini 2.5 Pro で編集ホットスポット再現率 67.3% [63.5–71.0%](n=600、KO+EN);KO+EN の人間文章コントロールで誤検出 16.0% [11.6–21.7%](n=200) | | **ライセンス** | MIT | @@ -140,7 +140,7 @@ patina persona new my-voice --describe "plain-spoken founder, casual" patina --persona my-voice draft.md # then reuse it ``` -ko/en/zh/ja で動作し、`--tone`/`--profile` と組み合わせられます(レジスター優先順位は `--tone` > persona > profile)。ペルソナはボイスを形づくるだけで意味フロアを下げることはありません。作成したペルソナは保存時に検証され、安全ゲートは MPS/忠実度 + 数値欠落チェックを引き続き強制します。 +ko/en/zh/ja で `--tone`/`--profile` と組み合わせられます。Node では persona が再利用ボイスを担い、レジスター優先順位は `--tone` > persona、profile はパターン方針を制御します。一部の skill/core-prompt 経路には旧 profile ボイス指示が残るため、この分離はまだ全経路の不変条件ではありません。どの制御も意味フロアを下げず、作成した persona は保存時に検証され、書き換えは MPS/忠実度と数値欠落チェックを引き続き強制します。 ## CI @@ -186,11 +186,11 @@ Input ```yaml # .patina.default.yaml -version: "6.3.4" +version: "7.0.0" language: ko # ko | en | zh | ja profile: default output: rewrite # rewrite | diff | audit | score -tone: # casual | professional | auto (register; genre = profile) +tone: # casual | professional | auto (register override; profile = pattern policy) ``` プロジェクトの `.patina.yaml` がデフォルトを上書きします。パターンパックは言語プレフィックスで自動検出されます。追加型のリストキー(`blocklist`、`allowlist`、`skip-patterns`)はマージされ、その他の配列は置き換えられます。 diff --git a/README_KR.md b/README_KR.md index 7429047..d22dcd4 100644 --- a/README_KR.md +++ b/README_KR.md @@ -6,7 +6,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Skill](https://img.shields.io/badge/Skill-Claude%20Code%20%7C%20Codex%20%7C%20Cursor%20%7C%20OpenCode-blueviolet)](#빠른-시작) [![Multi-language](https://img.shields.io/badge/Languages-KO%20%7C%20EN%20%7C%20ZH%20%7C%20JA-green)](https://github.com/devswha/patina) -[![Version](https://img.shields.io/badge/version-6.3.4-blue)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-7.0.0-blue)](CHANGELOG.md)

브라우저에서 바로 써보기 — 설치 없음 @@ -99,7 +99,7 @@ printf '%s\n' 'Coffee has emerged as a pivotal cultural phenomenon.' \ | **184개 패턴** | 언어별 재작성 가능 37개 + 스코어 전용 바이럴 훅 9개(KO/EN/ZH/JA 각각 46개) — 전체 184개 패턴 카탈로그는 [PATTERNS.md](docs/PATTERNS.md) 참고 | | **모드** | rewrite · verify · audit · score · diff | | **사용 채널** | 에이전트 스킬 · Node CLI · 페이지 내 preview · 브라우저 playground (리라이트 + 점수) | -| **보이스** | `--persona` (내장 + 직접 제작, ko/en/zh/ja) · `--tone` 격식 · `--profile` 장르 — 고정된 우선순위로 조합 가능 | +| **보이스 제어** | `--persona`는 재사용 보이스 · `--tone`은 register override · `--profile`은 Node 패턴 정책(일부 skill/core 경로에는 기존 profile voice guidance가 남아 있음) | | **무료 사용** | 로그인된 `codex`, `claude`, `gemini` CLI 중 하나로 `PATINA_API_KEY` 없이 재작성 실행 | | **캘리브레이션** | GPT-5.5 / Claude Sonnet 4.6 / Gemini 2.5 Pro 기준 편집 핫스팟 catch 67.3% [63.5–71.0%] (n=600, KO+EN); KO+EN 사람 글 컨트롤에서 오탐 16.0% [11.6–21.7%] (n=200) | | **라이선스** | MIT | @@ -140,7 +140,7 @@ patina persona new my-voice --describe "plain-spoken founder, casual" patina --persona my-voice draft.md # 이후 재사용 ``` -ko/en/zh/ja에서 동작하며 `--tone`/`--profile`과 조합됩니다(격식 우선순위 `--tone` > 페르소나 > 프로필). 페르소나는 말투를 바꿀 뿐 의미 하한을 낮추지 않습니다 — 제작한 페르소나는 저장 시 검증되고, 안전 게이트는 재작성 시 MPS/충실도 및 숫자 누락 검사를 그대로 강제합니다. +ko/en/zh/ja에서 `--tone`/`--profile`과 조합됩니다. Node에서는 persona가 재사용 보이스를 맡고 register 우선순위는 `--tone` > persona이며, profile은 패턴 정책을 제어합니다. 일부 skill/core-prompt 경로에는 기존 profile voice guidance가 남아 있어 이 구분이 아직 모든 경로의 불변식은 아닙니다. 어떤 제어도 의미 하한을 낮추지 않으며, 제작한 persona는 저장 시 검증되고 재작성은 MPS/충실도 및 숫자 누락 검사를 그대로 강제합니다. ## CI @@ -186,11 +186,11 @@ Input ```yaml # .patina.default.yaml -version: "6.3.4" +version: "7.0.0" language: ko # ko | en | zh | ja profile: default output: rewrite # rewrite | diff | audit | score -tone: # casual | professional | auto (register; genre = profile) +tone: # casual | professional | auto (register override; profile = pattern policy) ``` 프로젝트의 `.patina.yaml`이 기본값을 오버라이드합니다. 패턴 팩은 언어 접두사로 자동 탐색됩니다. 추가형 목록 키(`blocklist`, `allowlist`, `skip-patterns`)는 병합되고, 다른 배열은 대체됩니다. diff --git a/README_ZH.md b/README_ZH.md index e277afe..ae785f4 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -6,7 +6,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Skill](https://img.shields.io/badge/Skill-Claude%20Code%20%7C%20Codex%20%7C%20Cursor%20%7C%20OpenCode-blueviolet)](#快速开始) [![Multi-language](https://img.shields.io/badge/Languages-KO%20%7C%20EN%20%7C%20ZH%20%7C%20JA-green)](https://github.com/devswha/patina) -[![Version](https://img.shields.io/badge/version-6.3.4-blue)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-7.0.0-blue)](CHANGELOG.md)

去掉 AI 味,保留原意。 @@ -101,7 +101,7 @@ printf '%s\n' 'Coffee has emerged as a pivotal cultural phenomenon.' \ | **184 条模式** | 每种语言 37 条可改写模式 + 9 条仅评分的病毒式钩子模式(KO/EN/ZH/JA 各 46 条)—— 完整的 184 条模式目录见 [PATTERNS.md](docs/PATTERNS.md) | | **模式** | rewrite · verify · audit · score · diff | | **使用入口** | agent skill · Node CLI · 页面内 preview · 浏览器 playground(改写 + 评分) | -| **声音** | `--persona`(内置 + 自制,ko/en/zh/ja)· `--tone` 语域 · `--profile` 体裁 —— 按固定优先级可组合 | +| **声音控制** | `--persona` 是可复用声音 · `--tone` 覆盖语域 · `--profile` 设置 Node 模式策略(部分 skill/core 路径仍有旧的 profile 声音指导) | | **免费使用** | 已登录的 `codex`、`claude` 或 `gemini` CLI 可直接运行改写,无需 `PATINA_API_KEY` | | **校准** | 编辑热点命中率 67.3% [63.5–71.0%],跨 GPT-5.5 / Claude Sonnet 4.6 / Gemini 2.5 Pro(n=600,KO+EN);在 KO+EN 人类对照上误检率 16.0% [11.6–21.7%](n=200) | | **许可证** | MIT | @@ -142,7 +142,7 @@ patina persona new my-voice --describe "plain-spoken founder, casual" patina --persona my-voice draft.md # 之后复用 ``` -在 ko/en/zh/ja 上生效,并与 `--tone`/`--profile` 组合(语域优先级 `--tone` > 人格 > profile)。人格塑造声音,但绝不会降低含义下限 —— 自制人格在保存时会经过校验,安全闸门仍然强制 MPS/忠实度 + 数字缺失检查。 +可在 ko/en/zh/ja 上与 `--tone`/`--profile` 组合。Node 中,persona 负责可复用声音,语域优先级是 `--tone` > persona,profile 负责模式策略。部分 skill/core-prompt 路径仍保留旧的 profile 声音指导,因此这一区分尚未在所有路径中完全实现。任何控制都不会降低含义下限:自制 persona 会在保存时校验,改写仍强制执行 MPS/忠实度和数字缺失检查。 ## CI @@ -188,11 +188,11 @@ Input ```yaml # .patina.default.yaml -version: "6.3.4" +version: "7.0.0" language: ko # ko | en | zh | ja profile: default output: rewrite # rewrite | diff | audit | score -tone: # casual | professional | auto (register; genre = profile) +tone: # casual | professional | auto (register override; profile = pattern policy) ``` 项目级 `.patina.yaml` 会覆盖默认值。模式包按语言前缀自动发现。可追加的列表键(`blocklist`、`allowlist`、`skip-patterns`)会合并;其他数组会直接替换。 diff --git a/SKILL.md b/SKILL.md index 061cbce..261c89a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: patina -version: "6.3.4" +version: "7.0.0" description: Detect and rewrite AI writing patterns in Korean, English, Chinese, and Japanese text so it reads as if a human wrote it. Meaning-preservation (MPS) verified. allowed-tools: - Read @@ -39,8 +39,7 @@ Glob .patina.default.yaml → Read - `--diff`: diff 출력 모드 - `--audit`: audit 출력 모드 - `--score`: score 출력 모드 -- `--ouroboros`: ouroboros 모드 (반복 교정 + 점수 수렴) -- `--strict`: 옵트인 다중 패스 엄격 모드. rewrite 출력 모드를 사용한다. `--lang`, `--tone`, `--profile`, `--ouroboros`와 함께 사용할 수 있다. `--audit`, `--diff`, `--score`와 함께 사용할 수 없다. +- `--strict`: 옵트인 다중 패스 엄격 모드. rewrite 출력 모드를 사용한다. `--lang`, `--tone`, `--profile`와 함께 사용할 수 있다. `--audit`, `--diff`, `--score`와 함께 사용할 수 없다. - `--lang `: 처리 언어 변경 (ko, en, zh, ja). 설정 파일의 `language` 값을 오버라이드한다. - `--tone `: 어투(레지스터) 지정. 유효값: `casual | professional | auto`. academic/marketing/narrative/instructional 은 장르이므로 `--profile ` 을 쓴다. 알 수 없는 값이면 즉시 오류: "Unknown tone ''. Valid tones: casual, professional, auto" - `--batch `: 여러 파일을 한꺼번에 처리 (glob 또는 명시적 경로 목록). @@ -70,12 +69,10 @@ if --lang in {zh, ja} and resolved_tone != null: → profile-only 경로로 fallback ``` -**legal/medical fidelity 보존 (R2):** resolved_tone=professional이더라도 config.profile이 `legal` 또는 `medical`이면 `ouroboros.combined-weights.legal/medical` (fidelity 0.65)을 강제 적용한다. +**legal/medical fidelity 보존 (R2):** resolved_tone=professional이더라도 config.profile이 `legal` 또는 `medical`이면 `scoring.combined-weights.legal/medical` (fidelity 0.65)을 강제 적용한다. -`--score` 또는 `--ouroboros` 또는 `ouroboros.enabled: true`이면 `core/scoring.md`도 로드한다. +`--score`이면 `core/scoring.md`도 로드한다. 파일이 없으면 에러: "core/scoring.md not found. Please update patina." - -`--ouroboros`는 rewrite 출력 모드를 사용한다. `--audit`, `--diff`, `--score`와 함께 사용할 수 없다. `--strict`는 rewrite 출력 모드를 사용한다. `--audit`, `--diff`, `--score`와 함께 사용할 수 없다. --- @@ -134,7 +131,7 @@ Read core/voice.md > **주의:** 텍스트가 1개 단락 이하이고 2개 문장 이하이면 추출을 건너뛰고 파이프라인을 정상 실행한다 (오버헤드 불필요). > -> **참고:** 앵커 추출이 건너뛰어지거나 추출된 앵커가 0개이면, MPS는 N/A로 표시되며 ouroboros의 MPS 게이팅을 면제한다. +> **참고:** 앵커 추출이 건너뛰어지거나 추출된 앵커가 0개이면, MPS는 N/A로 표시되며 검증 게이트의 MPS 하한을 적용하지 않는다. ### 앵커 유형 @@ -392,52 +389,6 @@ document is SUSPECT iff --- -## Ouroboros 루프 (`--ouroboros`) - -`--ouroboros` 플래그가 있거나 `ouroboros.enabled: true`이면, 아래 루프가 5단계와 6단계를 감싼다. -1~4단계(설정, 패턴, 프로필, 목소리 로드)는 한 번만 실행한다. - -### 절차 - -1. **초기 점수 측정**: 입력 텍스트에 대해 score 알고리즘(6단계 score 모드)을 실행하여 초기 점수를 산출한다 - - 초기 점수가 이미 target-score 이하이면 "이미 목표 달성"으로 즉시 종료한다 - -2. **반복 실행** (iteration = 1부터): - a. 5a단계(구조) → 5b단계(문장/어휘) → 5c단계(자기검수) 파이프라인을 실행한다 - b. 교정된 텍스트에 대해 score 알고리즘을 다시 실행한다 - c. delta = 이전 점수 - 현재 점수 (양수 = 개선) - d. 종료 조건을 확인한다: - - 현재 점수 ≤ target-score → 종료 사유: **목표 달성** - - delta < 0 (점수가 오히려 올라감) → 종료 사유: **점수 회귀** → 이전 반복 결과로 롤백 - - 0 ≤ delta ≤ plateau-threshold → 종료 사유: **개선 정체** - - iteration ≥ max-iterations → 종료 사유: **반복 상한** - - 충실도 점수 < fidelity-floor (기본: 70) → 종료 사유: **충실도 하한 위반** → 이전 반복 결과로 롤백 - - MPS < mps-floor (기본: 70) → 종료 사유: **의미 보존 하한 위반** → 이전 반복 결과로 롤백 - e. 종료 조건 미충족 시 교정된 텍스트를 입력으로 다음 반복 - -3. **출력 형식**: - -#### Ouroboros 반복 로그 - -| 반복 | 점수 (전) | 점수 (후) | 개선량 | 종료 사유 | -|------|-----------|-----------|--------|-----------| -| 0 | — | 78 | — | 초기 측정 | -| 1 | 78 | 45 | +33 | | -| 2 | 45 | 28 | +17 | 목표 달성 | - -#### 최종 결과 -- 최종 점수: 28/100 (±10) -- 반복 횟수: 2/3 -- 종료 사유: 목표 달성 (target: 30) - -[최종 교정 텍스트] - -> **주의:** `--ouroboros`는 rewrite 출력을 기본으로 한다. `--diff`, `--audit`, `--score`와 함께 사용할 수 없다. - ---- - - ---- ## Strict 모드 (다중 패스) (`--strict`) @@ -497,9 +448,9 @@ P2 출력에 4.6/4.7단계의 통계 감지와 AI-lexicon 매칭을 재실행하 #### P5: 수락/재시도/롤백 게이트 (Accept / Retry / Rollback Gate) ``` -floors (ouroboros와 동일): - fidelity_floor = 70 - mps_floor = 70 +floors: + fidelity_floor = verification.fidelity-floor (default: 70) + mps_floor = verification.mps-floor (default: 70) accept 조건 (모두 충족): fidelity_score >= fidelity_floor @@ -538,7 +489,6 @@ gate_result: accept | rollback --- ``` -> **참고:** `--strict`는 `--ouroboros`와 함께 사용할 수 있다. 이 경우 P5 게이트를 통과한 최종 출력이 ouroboros 루프의 입력으로 사용된다. ## 배치 모드 (`--batch`) @@ -577,7 +527,6 @@ Glob {지정된 파일 패턴} → 파일 목록 확보 | big.md | — | — | — | ⏭️ skipped (too large) | | broken.md | — | — | — | ❌ error: parse failed | -> **주의:** `--batch`는 `--ouroboros`와 함께 사용할 수 있다. 이 경우 각 파일에 대해 ouroboros 루프가 독립적으로 실행된다. --- @@ -660,7 +609,7 @@ FOR each anchor IN anchor_list: | `casual` | 14:suppress, 15:reduce, 17:reduce, 18:amplify, 8:amplify | 14:suppress, 15:reduce, 17:reduce, 7:amplify, 8:amplify | | `professional` | 17:suppress, 25:reduce, 28:amplify | 17:suppress, 25:reduce, 28:amplify | - **legal/medical fidelity 강제 (R2):** resolved_tone=professional이고 config.profile이 `legal` 또는 `medical`이면, `ouroboros.combined-weights.legal` / `ouroboros.combined-weights.medical` (fidelity 0.65)을 강제 적용한다. 톤 오버라이드가 fidelity 하한을 낮추지 않도록 한다. + **legal/medical fidelity 강제 (R2):** resolved_tone=professional이고 config.profile이 `legal` 또는 `medical`이면, `scoring.combined-weights.legal` / `scoring.combined-weights.medical` (fidelity 0.65)을 강제 적용한다. 톤 오버라이드가 fidelity 하한을 낮추지 않도록 한다. 9. **의미 보존 제약 주입** — 의미 위험도가 HIGH인 패턴을 적용할 때, 해당 문단의 앵커를 교정 프롬프트에 포함한다: "다음 주장을 반드시 유지하라: {앵커 목록}". MEDIUM 위험도 패턴은 극성(Polarity) 또는 부정(Negation) 앵커가 있는 문단에서만 제약을 주입한다. LOW 위험도 패턴은 제약 없이 적용한다. @@ -684,7 +633,7 @@ FOR each anchor IN anchor_list: 2. **최종 앵커 대조** — 전체 앵커 목록과 최종 결과물을 비교한다. 5a-v/5b-v에서 미처리된 HARD FAIL 앵커가 있으면 해당 문장을 원문으로 복원한다 (안전망) 3. **극성 반전 스캔** — 원문의 부정이 긍정으로(또는 반대) 바뀐 곳을 명시적으로 탐색한다. 부정어, 비교 표현, 조건절에 집중한다 4. **회귀 체크** — 5a단계 출력과 최종 출력을 비교하여, 5a 교정이 되돌려진 구간이 있으면 5a 교정을 재적용한다 -5. **MPS 산출** — 앵커 검증 결과로부터 MPS(Meaning Preservation Score)를 계산한다. `--score` 또는 `--ouroboros` 모드일 때 출력에 포함한다 +5. **MPS 산출** — 앵커 검증 결과로부터 MPS(Meaning Preservation Score)를 계산한다. `--score` 모드와 Strict P5 검증에서 사용한다 --- @@ -743,7 +692,7 @@ AI 유사도 점수를 0-100 척도로 산출한다. `core/scoring.md`를 참조 - 카테고리 점수 = (조정된 심각도 합계 / (패턴 수 × 3)) × 100 - 패턴 수는 팩 frontmatter의 `patterns` 필드를 사용한다 4. **전체 점수 계산**: 카테고리 점수의 가중 평균 - - 가중치는 `ouroboros.category-weights.{lang}` 설정을 사용한다 + - 가중치는 `scoring.category-weights.{lang}` 설정을 사용한다 - 설정에 없는 카테고리(커스텀 팩)는 기본 가중치 0.10을 사용한다 5. **출력 형식**: @@ -763,9 +712,9 @@ AI 유사도 점수를 0-100 척도로 산출한다. `core/scoring.md`를 참조 점수 해석: 0-15 사람다움 / 16-30 거의 사람다움 / 31-50 혼재 / 51-70 AI 느낌 / 71-100 AI 생성 -### Fidelity 점수 (rewrite/ouroboros 모드에서만) +### Fidelity 점수 -`--score`가 rewrite 또는 ouroboros 모드와 함께 사용되면 (원본 텍스트가 있는 경우), 원본 대비 의미 보존도를 추가로 측정한다. `core/scoring.md` §§ 9-13의 절차를 따른다: +원본 텍스트가 있는 점수 산출에서는 원본 대비 의미 보존도를 추가로 측정한다. `core/scoring.md` §§ 9-13의 절차를 따른다: 1. **Claims Preserved** — 원본의 사실적 주장이 교정본에 보존되었는지 (0-3) 2. **No Fabrication** — 교정본에 원본에 없는 내용이 추가되지 않았는지 (0-3) @@ -782,7 +731,7 @@ AI 유사도 점수를 0-100 척도로 산출한다. `core/scoring.md`를 참조 의미 보존 점수(MPS)는 4.5단계에서 추출된 의미 앵커가 최종 결과물에 얼마나 보존되었는지를 측정한다. `core/scoring.md` §14를 참조한다. 종합 점수 = `(AI 유사도 × ai_weight) + ((100 - 충실도) × fidelity_weight)`. -가중치는 `ouroboros.combined-weights.{profile}` 설정에 따른다 (기본: AI 0.60, 충실도 0.40). +가중치는 `scoring.combined-weights.{profile}` 설정에 따른다 (기본: AI 0.60, 충실도 0.40). > **참고:** 점수는 LLM의 심각도 판단에 기반하므로 ±8-10 포인트의 변동이 있을 수 있다. > 정확한 수치보다 범위로 해석한다. diff --git a/SUPPORT.md b/SUPPORT.md index f71f7b6..a1951d6 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -15,7 +15,7 @@ Please include: - patina version or commit; - language (`ko`, `en`, `zh`, `ja`); -- mode (`rewrite`, `audit`, `score`, `diff`, `ouroboros`); +- mode (`rewrite`, `audit`, `score`, `diff`, or `strict`); - backend/provider if relevant; - a short input sample, if it is safe to share; - expected vs actual output. diff --git a/agents/patina-fidelity-auditor.md b/agents/patina-fidelity-auditor.md index e20e1f9..acfa8f1 100644 --- a/agents/patina-fidelity-auditor.md +++ b/agents/patina-fidelity-auditor.md @@ -13,7 +13,7 @@ Given an ORIGINAL text and a REWRITE, verify that the rewrite faithfully preserv ## Prerequisites -Read `core/scoring.md` §§9-14 before auditing. The fidelity criteria, scoring formula, floor thresholds, and ouroboros termination conditions are defined there and must be applied as written. +Read `core/scoring.md` §§9-14 before auditing. The fidelity criteria, scoring formula, and verification floors defined there must be applied as written. ## Patina's four non-negotiable principles (audit against all four) @@ -80,14 +80,14 @@ These are not scored separately but must be explicitly confirmed or flagged: fidelity_score = ((claims + fabrication + tone + length) / 12) × 100 ``` -Ouroboros floors (`core/scoring.md` §13): fidelity_score ≥ 70 is required; below 70 is a hard stop regardless of AI-likeness improvement. +Verification floors (`core/scoring.md` §13): fidelity_score ≥ `verification.fidelity-floor` (default: 70) is required; a result below the floor must be retried or rolled back regardless of AI-likeness improvement. ## Verdict Output one of: -- **PASS** — fidelity_score ≥ 70 and no individual criterion is Fail, and all MPS-level checks confirm. The rewrite may proceed. -- **NEEDS-ROLLBACK** — fidelity_score < 70, or any criterion is Fail (0), or any MPS-level check fails. The rewrite must be discarded or revised before use. +- **PASS** — fidelity_score ≥ `verification.fidelity-floor` (default: 70), no individual criterion is Fail, and all MPS-level checks confirm. The rewrite may proceed. +- **NEEDS-ROLLBACK** — fidelity_score < `verification.fidelity-floor`, any criterion is Fail (0), or any MPS-level check fails. The rewrite must be discarded or revised before use. ## Output format diff --git a/core/scoring.md b/core/scoring.md index 4409870..bc97327 100644 --- a/core/scoring.md +++ b/core/scoring.md @@ -1,15 +1,14 @@ --- name: AI-Likeness Scoring Algorithm version: 1.0.0 -description: Pattern-based AI-likeness scoring reference for patina score and ouroboros modes +description: Pattern-based AI-likeness scoring reference for patina score and verification --- # AI-Likeness Scoring Algorithm Pattern-based scoring that converts AI pattern detection results into a numeric 0-100 score. -Used by `--score` mode and `--ouroboros` loop for termination gating. +Used by `--score` mode and strict verification. ---- ## 1. Severity Scale (Per-Detection) @@ -23,6 +22,11 @@ depending on how egregiously it appears in context. | Medium | 2 | Pattern present at moderate frequency or impact | | Low | 1 | Pattern barely present — isolated occurrence | | Not detected | 0 | Pattern not found in text | +`scoring.severity-points` configures the numeric values for `high`, `medium`, and +`low` severity (defaults: 3, 2, and 1). + +`scoring.deterministic` configures the deterministic shadow score used for +reproducible drift checks; the LLM score remains canonical. --- @@ -123,9 +127,9 @@ Unknown categories (from custom packs not in the weight config) get default weig | viral-hook | 0.10 | 9 | score-only (no rewrite) | | **Total** | **1.00** | **46** | | -Weights are configurable via `ouroboros.category-weights.{lang}` in `.patina.yaml`. +Weights are configurable via `scoring.category-weights.{lang}` in `.patina.yaml`. -**viral-hook weight review (issue #154):** kept at `0.10`. The pack remains score-only and the three added patterns improve the category denominator/granularity rather than broadening it enough to justify a larger score contribution. Rewrite, diff, and Ouroboros modes still skip the pack. +**viral-hook weight review (issue #154):** kept at `0.10`. The pack remains score-only and the three added patterns improve the category denominator/granularity rather than broadening it enough to justify a larger score contribution. Rewrite and diff modes still skip the pack. --- @@ -246,7 +250,7 @@ Interpretation: 0-15 range = "사람다움" (Strongly human-like) - **LLM non-determinism** means the same text may score differently across runs. The formula is deterministic; the severity assignment is not. - **Fidelity scoring** (meaning preservation vs original) is defined in §§ 9–13 below - and integrated into `--score` and `--ouroboros` pipelines. + and used by scoring and strict verification. ### Short-text boost (v3.11 Phase 3.2) @@ -276,7 +280,7 @@ Fidelity measures *how faithfully the output preserves the original meaning*. Both dimensions are necessary: aggressive humanization can achieve a low AI score by deleting content or changing meaning entirely. Fidelity scoring guards against this. -Fidelity scoring is integrated into `--score` and `--ouroboros` pipelines. +Fidelity scoring is used by scoring and strict verification. See SKILL.md § 6 (score mode) for integration details. --- @@ -385,26 +389,10 @@ fidelity_score = ((claims + fabrication + tone + length) / 12) × 100 - Maximum: (3+3+3+3) / 12 × 100 = **100** (perfect fidelity) - Minimum: (0+0+0+0) / 12 × 100 = **0** (total meaning loss) -### Criterion Weighting (Optional) - -For profiles that need non-uniform criterion importance, weights can be configured: +### Criterion Weighting -```yaml -fidelity: - weights: - claims-preserved: 0.35 - no-fabrication: 0.30 - tone-match: 0.20 - length-ratio: 0.15 -``` - -When weights are configured: - -``` -fidelity_score = Σ((criterion_points / 3) × criterion_weight) × 100 -``` - -Default weights (when not configured): equal at 0.25 each (equivalent to the simple formula). +The four fidelity criteria have equal weight (0.25 each), which is equivalent to the +simple formula above. ### Worked Example @@ -468,10 +456,9 @@ Where: | Marketing profile | 0.65 | 0.35 | Tone transformation tolerated, creative rewriting expected | | Namuwiki profile | 0.65 | 0.35 | Tone transformation tolerated for wiki-style cleanup | -Configurable via `ouroboros.combined-weights.{profile}` in `.patina.yaml`: - +Configurable via `scoring.combined-weights.{profile}` in `.patina.yaml`: ```yaml -ouroboros: +scoring: combined-weights: default: ai-likeness: 0.60 @@ -491,13 +478,13 @@ ouroboros: | 51-70 | 주의 | Caution — significant AI traces or meaning loss | | 71-100 | 부적합 | Poor — heavy AI patterns and/or substantial meaning loss | -### Ouroboros Termination +### Verification Floors -When used with `--ouroboros`, the loop terminates when: -- Combined score ≤ threshold (default: 30), OR -- Fidelity score drops below floor (default: 70) — **hard stop**, even if AI score improves +Strict verification accepts a rewritten result only when both configured floors pass: +- Fidelity score ≥ `verification.fidelity-floor` (default: 70) +- MPS ≥ `verification.mps-floor` (default: 70) -This prevents the ouroboros loop from "improving" AI score by destroying content. +A failed floor requires retrying the affected span or restoring its original text. This prevents a lower AI score from being accepted at the cost of meaning preservation. --- @@ -578,7 +565,7 @@ MPS = N/A (not applicable) When MPS = N/A: - `--score` mode displays: `의미 보존 (MPS): N/A (앵커 없음)` -- Ouroboros loop: MPS floor는 우회되지 않는다. 결정론 루프의 `scoreMPS`는 숫자 mps를 반환하거나 스코어러 실패 시 `null`을 반환하며, `null`은 floor 위반으로 간주해 fail-closed로 직전 반복에 rollback한다 (아래 "Ouroboros Loop Gating" 및 `src/ouroboros.js` 참고). 앵커 N/A 표시는 SKILL 파이프라인 / `--score` 표시용이며 결정론 루프의 floor를 끄지 않는다. +- Strict verification does not apply the MPS floor when no anchors were extracted. ### MPS Interpretation @@ -630,8 +617,7 @@ MPS measures **humanization coverage** — what fraction of meaning anchors were ### `--score` Mode Output -When `--score` is used with rewrite or ouroboros mode (original text available), -MPS is displayed alongside AI-likeness and Fidelity: +When a scored rewrite has the original text available, MPS is displayed alongside AI-likeness and Fidelity: | 지표 | 점수 | |------|------| @@ -644,18 +630,8 @@ MPS is displayed alongside AI-likeness and Fidelity: > Combined score uses fidelity (holistic) while MPS is a structural verification metric. > Both are displayed for transparency but serve different purposes. -### Ouroboros Loop Gating - -MPS floor = 70 (default). Independent of fidelity floor. - -Termination condition: -- MPS < mps-floor → terminate with reason: **의미 보존 하한 위반** → rollback to previous iteration +### Strict Verification Gate -Both fidelity floor AND MPS floor must pass for an iteration to be accepted. - -Configurable via `.patina.yaml`: - -```yaml -ouroboros: - mps-floor: 70 # default -``` +MPS and fidelity are checked independently against `verification.mps-floor` and +`verification.fidelity-floor`; both numeric scores must meet their configured floors +before the rewrite is accepted. diff --git a/core/standalone-prompt.md b/core/standalone-prompt.md index 781f3d0..69d29b2 100644 --- a/core/standalone-prompt.md +++ b/core/standalone-prompt.md @@ -20,7 +20,7 @@ The user provides: config: language: ko # ko | en | zh | ja profile: default # default | blog | academic | technical | formal | social | email | legal | medical | marketing - output: rewrite # rewrite | diff | audit | score | ouroboros + output: rewrite # rewrite | diff | audit | score skip-patterns: [] # e.g., [ko-filler] blocklist: [] # extra words to flag allowlist: [] # words to never flag @@ -29,7 +29,7 @@ text: | [The user's text to humanize goes here] ``` -Override per-run: the host system may allow `--lang`, `--profile`, `--diff`, `--audit`, `--score`, `--ouroboros` flags. +Override per-run: the host system may allow `--lang`, `--profile`, `--diff`, `--audit`, and `--score` flags. --- @@ -51,7 +51,7 @@ Read `profiles/{profile}.md`. Parse `voice-overrides` and `pattern-overrides`. ### 4. Load Voice Guidelines Read `core/voice.md`. Apply `voice-overrides` from the profile. -### 5. Load Scoring Reference (if score or ouroboros mode) +### 5. Load Scoring Reference (if score mode) Read `core/scoring.md`. --- @@ -62,7 +62,7 @@ Read `core/scoring.md`. Before rewriting, extract semantic anchors from the input text. These are internal working memory only — do NOT show them to the user. -**Skip condition**: If text is ≤1 paragraph and ≤2 sentences, skip extraction. MPS is marked N/A and ouroboros MPS gating is bypassed. +**Skip condition**: If text is ≤1 paragraph and ≤2 sentences, skip extraction. MPS is marked N/A and no MPS floor is applied. **Anchor types**: @@ -114,7 +114,7 @@ FOR each anchor IN anchor_list: 2. Inject constraint: "You must preserve: {anchor content}". 3. Compare retry result against the anchor. 4. If retry also fails → HARD FAIL (restore original). -5. Max 1 retry per anchor (no retry loops). +5. Max 1 retry per anchor (no repeated retries). --- @@ -151,7 +151,7 @@ Same logic as 5a-v. Additionally: 2. **Final anchor check** — any HARD FAIL anchors not yet handled? Restore original sentences (safety net). 3. **Polarity inversion scan** — explicitly search where original negation became positive (or vice versa). Focus on negatives, comparatives, conditionals. 4. **Regression check** — compare 5a output vs final output. Re-apply any reverted 5a corrections. -5. **MPS calculation** — calculate Meaning Preservation Score from anchor verification results. Include in output for score/ouroboros modes. +5. **MPS calculation** — calculate Meaning Preservation Score from anchor verification results. Include it in score output when available. --- @@ -211,7 +211,7 @@ Fidelity criteria (each 0-3): Combined = `(ai_likeness × ai_weight) + ((100 - fidelity) × fidelity_weight)` -Weights per profile (from `.patina.default.yaml`): +Weights per profile (from `scoring.combined-weights` in `.patina.default.yaml`): - default: AI 0.60, fidelity 0.40 - academic: AI 0.40, fidelity 0.60 - blog: AI 0.70, fidelity 0.30 @@ -222,28 +222,6 @@ Weights per profile (from `.patina.default.yaml`): - medical: AI 0.35, fidelity 0.65 - marketing: AI 0.65, fidelity 0.35 -### Ouroboros Mode - -Iterative self-improvement loop: - -1. Measure initial score -2. If already ≤ target-score, stop immediately -3. Repeat (max 3 iterations by default): - a. Run 5a → 5b → 5c pipeline - b. Score the result - c. delta = previous - current (positive = improvement) - d. Check termination: - - Score ≤ target-score → **target met** - - delta < 0 → **regression** → rollback - - 0 ≤ delta ≤ plateau-threshold → **plateau** - - iteration ≥ max-iterations → **max iterations** - - fidelity < fidelity-floor → **fidelity violation** → rollback - - MPS < mps-floor → **MPS violation** → rollback -4. Output iteration log and final text - -**Ouroboros cannot be combined with diff, audit, or score modes.** - ---- ## Batch Mode @@ -281,7 +259,7 @@ Special cases: ### Per-Category Score ``` -category_score = (sum of adjusted severities / (pattern_count × 3)) × 100 +category_score = (sum of adjusted severities / (pattern_count × high severity points)) × 100 ``` ### Overall Score @@ -329,7 +307,7 @@ If no anchors extracted: `MPS = N/A` - **Match profile tone**: or the profile's target tone if explicitly overridden. - **Inject voice**: follow `core/voice.md` per language. - **Apply overrides**: respect `pattern-overrides` and `voice-overrides`. -- **No infinite loops**: self-audit runs once. Ouroboros has max-iterations cap. +- **Bounded verification**: self-audit runs once; each anchor has at most one retry before its original sentence is restored. - **Scores have variance**: ±8-10 points between runs due to LLM severity assignment. Interpret ranges, not exact numbers. --- diff --git a/docs/API.md b/docs/API.md index f505a28..99f12ff 100644 --- a/docs/API.md +++ b/docs/API.md @@ -126,9 +126,6 @@ throw new PatinaCliError({ what: 'missing input', why: 'No file was provided', a

DEFAULT_HTTP_KEY_ENV_VARS : Array.<string>

Default key lookup order for the OpenAI-compatible HTTP provider.

-
PERSONA_LANGSObject
-

Create a SIGINT-aware cancellation controller for long-running CLI operations.

-
MAX_INPUT_BYTES

Maximum size (in bytes) of a single input file patina will read into memory. Guards against accidental memory exhaustion on huge or binary inputs (#508 G1).

@@ -138,7 +135,7 @@ Guards against accidental memory exhaustion on huge or binary inputs (#508 G1).<
DEFAULT_SEVERITY_POINTS : Readonly.<{high: number, medium: number, low: number}>

Default per-detection severity points.

-

Mirrors ouroboros.severity-points in .patina.default.yaml and the +

Mirrors scoring.severity-points in .patina.default.yaml and the core/scoring.md §1 table (both gated by tests/unit/threshold-parity.test.js). buildScoreMathCore derives the prompt's severity-scale line and the category-score denominator from these values via resolveSeverityPoints, @@ -170,6 +167,15 @@ and the model verdict is hot; absent model means baseline behavior.

## Functions
+
isTemperatureRejectedError(err)boolean
+

True when the provider rejected a request solely because temperature is +unsupported/deprecated for the requested model. Callers retry exactly once +with the field omitted (and remember the model for this process).

+
+
modelRejectsTemperature(model)boolean
+
+
markTemperatureRejected(model)void
+
redactErrorText(text)string

Redact secret-bearing substrings (Bearer tokens, sk- API keys, key= query params) from provider error text BEFORE it enters an error message, error @@ -177,13 +183,17 @@ body, or a log line. The single source of truth for LLM-transport error redaction, reused by the streaming helper and the scoring logger so a BYOK key echoed in a provider error response is never persisted (AC11).

+
dispatchMetadata(callback, metadata)void
+

Invoke an optional metadata callback without allowing consumer code to affect +a paid provider request or its result.

+
isRetryable(err)boolean

Decide whether an LLM call failure should be retried.

computeBackoffMs(attempt, retryAfter, [opts])number

Compute retry delay from Retry-After or exponential backoff with jitter.

-
readStreamedCompletion(response)Promise.<{choices: Array.<{message: {content: string}, finish_reason: string}>, model: string, usage: object}>
+
readStreamedCompletion(response, [onMetadata])Promise.<{choices: Array.<{message: {content: string}, finish_reason: string}>, model: string, usage: object}>

Read a streamed (SSE) chat-completions response and assemble it into the non-streaming response shape (choices[0].message.content plus model / usage / finish_reason when the provider sends them), so the rest of @@ -216,9 +226,21 @@ writes a byte-preserving output atomically. --dry-run reports the plan with zero LLM calls and no writes. Language/patterns are resolved per file from the XLIFF target-language (cached), not the global config language.

+
createCancellationController([options])Object
+

Create a SIGINT-aware cancellation controller for long-running CLI operations.

+
resolveProfileForLanguage(profileName, lang, [logger])string

Resolve a profile name against language-specific profile limits.

+
warnIfAlreadyHuman()
+

Over-editing guard (Study 1 RQ5b): rewriting text that already reads human +measurably nudged it TOWARD AI-likeness (+3.3 judged points on human English +documents, docs/research/2026-rewrite-efficacy-study1.md). When the +deterministic layer finds nothing to fix, say so before spending a rewrite — +advisory only, never blocks, and silent wherever the deterministic score is +unavailable or the text is too short to judge (Study 0 Deviation 1). +Opt out with over-editing-guard: false in config.

+
loadConfig([path], [opts])object

Load default config and merge global/project .patina.yaml overrides.

Precedence (low → high): base path → ~/.patina.yaml → ./.patina.yaml → overridePath. @@ -257,7 +279,10 @@ the runtime error's 1 (#526).

Split Markdown-style YAML frontmatter from a document body.

loadPatterns(repoRoot, lang, [skipPatterns])Array.<{file: string, frontmatter: (object|null), body: string, isStructure: boolean, isScoreOnly: boolean}>
-

Load language-specific pattern packs from patterns/{lang}-*.md.

+

Load language-specific pattern packs from patterns/{lang}-.md, plus any +user or pro packs in custom/patterns/{lang}-.md. On a filename collision +the custom pack wins (same precedence the persona and lexicon loaders give +custom/), so an installed pack can also override a built-in one.

loadProfile(repoRoot, profileName)Object

Load a named profile from profiles/{profileName}.md after path validation.

@@ -283,9 +308,6 @@ matching override are returned unchanged (same object identity).

createLogger([options])Object

Create a small stderr logger with text and progress modes.

-
runOuroboros(options)Promise.<{finalText: string, finalScore: number, iterations: number, reason: string, log: Array.<object>}>
-

Run the iterative Ouroboros rewrite-and-score loop.

-
formatOutput(result, mode, [parsed], [opts])string

Format a raw backend result for CLI output mode and requested format.

@@ -316,6 +338,15 @@ computed deterministically so they appear regardless of which model ran. ko translationese rules are listed even below the hot-density gate, because audit is a hint surface, not a verdict.

+
splitPromptForCaching(prompt, [minPrefixChars])Object
+

Split a built prompt into a cacheable static prefix and a dynamic tail for +provider prompt caching. The prefix is everything before the FIRST input +fence: on a first-turn prompt that is the full static catalog (identical +across requests for a given lang/profile/persona), while refine prompts +carry variable fenced references near the top, so their prefix falls under +the minimum and caching is skipped — avoiding cache writes that would never +be re-read.

+
fenceReferenceText(text, [options])string

Fence an untrusted REFERENCE block (not the rewrite target) as treat-as-data, with a trusted label describing its role. Used by the web refine path so the @@ -326,7 +357,7 @@ own output is unchanged.

resolveSeverityPoints([config])Object

Resolve the effective per-detection severity points for a config.

Single resolution path for every prompt surface: yaml -ouroboros.severity-points overrides the documented defaults key-by-key.

+scoring.severity-points overrides the documented defaults key-by-key.

buildPrompt(options)string

Build the LLM prompt for rewrite, diff, audit, or score mode.

@@ -349,6 +380,10 @@ single prompt can never carry two contradictory contracts (issue #397).

resolveProviderConfig(options)Object

Resolve effective API key, base URL, and model from explicit values, provider, and env.

+
validateAttemptRecord(value)
+
+
notifyInvalidAttempt(onAttemptInvalid)
+
scoreText(options)Promise.<object>

Score text for AI-likeness using an LLM JSON scorer plus deterministic shadow signals.

@@ -446,26 +481,6 @@ Default key lookup order for the OpenAI-compatible HTTP provider. ```js const first = DEFAULT_HTTP_KEY_ENV_VARS[0]; // PATINA_API_KEY ``` - - -## PERSONA\_LANGS ⇒ Object -Create a SIGINT-aware cancellation controller for long-running CLI operations. - -**Kind**: global constant -**Returns**: Object - Controller facade. - -| Param | Type | Default | Description | -| --- | --- | --- | --- | -| [options] | object | | Cancellation integration points. | -| [options.processObj] | NodeJS.Process | process | Process-like object used for signal listeners. | -| [options.stderr] | NodeJS.WritableStream | process.stderr | Stream for fallback cancel messages. | -| [options.logger] | object \| null | | Optional patina logger. | - -**Example** -```js -const cancellation = createCancellationController(); -cancellation.install(); -``` ## MAX\_INPUT\_BYTES @@ -488,7 +503,7 @@ defaultLogger.info('patina.ready', { message: 'ready' }); ## DEFAULT\_SEVERITY\_POINTS : Readonly.<{high: number, medium: number, low: number}> Default per-detection severity points. -Mirrors `ouroboros.severity-points` in .patina.default.yaml and the +Mirrors `scoring.severity-points` in .patina.default.yaml and the core/scoring.md §1 table (both gated by tests/unit/threshold-parity.test.js). `buildScoreMathCore` derives the prompt's severity-scale line and the category-score denominator from these values via `resolveSeverityPoints`, @@ -537,6 +552,38 @@ It only affects the deterministic score when a private local model is loaded and the model verdict is hot; absent model means baseline behavior. **Kind**: global constant + + +## isTemperatureRejectedError(err) ⇒ boolean +True when the provider rejected a request solely because `temperature` is +unsupported/deprecated for the requested model. Callers retry exactly once +with the field omitted (and remember the model for this process). + +**Kind**: global function + +| Param | Type | +| --- | --- | +| err | unknown | + + + +## modelRejectsTemperature(model) ⇒ boolean +**Kind**: global function +**Returns**: boolean - Whether this process already saw the model reject `temperature`. + +| Param | Type | +| --- | --- | +| model | string | + + + +## markTemperatureRejected(model) ⇒ void +**Kind**: global function + +| Param | Type | +| --- | --- | +| model | string | + ## redactErrorText(text) ⇒ string @@ -552,6 +599,19 @@ key echoed in a provider error response is never persisted (AC11). | --- | --- | | text | unknown | + + +## dispatchMetadata(callback, metadata) ⇒ void +Invoke an optional metadata callback without allowing consumer code to affect +a paid provider request or its result. + +**Kind**: global function + +| Param | Type | +| --- | --- | +| callback | function \| undefined | +| metadata | object | + ## isRetryable(err) ⇒ boolean @@ -592,7 +652,7 @@ const delay = computeBackoffMs(1, '2'); // 2000 ``` -## readStreamedCompletion(response) ⇒ Promise.<{choices: Array.<{message: {content: string}, finish\_reason: string}>, model: string, usage: object}> +## readStreamedCompletion(response, [onMetadata]) ⇒ Promise.<{choices: Array.<{message: {content: string}, finish\_reason: string}>, model: string, usage: object}> Read a streamed (SSE) chat-completions response and assemble it into the non-streaming response shape (`choices[0].message.content` plus `model` / `usage` / `finish_reason` when the provider sends them), so the rest of @@ -603,6 +663,7 @@ callLLM stays transport-agnostic (#576). | Param | Type | Description | | --- | --- | --- | | response | Object | Fetch response with an SSE body. | +| [onMetadata] | function | Optional metadata callback. | @@ -636,12 +697,14 @@ Call an OpenAI-compatible chat completions endpoint with retries, timeout, and a | [options.temperature] | number | DEFAULT_TEMPERATURE | Sampling temperature. | | [options.seed] | number \| string | | Optional deterministic seed forwarded to the provider. | | [options.responseFormat] | object | | Optional OpenAI-compatible structured-output request field (sent as response_format) when provided. | +| [options.extraBody] | object | | Optional provider-specific fields spread into the OpenAI-compat request body (protocol fields cannot be overridden; ignored on the native Anthropic path). | | [options.timeout] | number | 120000 | Per-attempt timeout in milliseconds. Budgets above 300s automatically switch the request to SSE streaming so undici's headersTimeout cannot kill long-running local backends (#576). | | [options.maxRetries] | number | 2 | Retry count after the first attempt. | | [options.deadline] | number | | Absolute epoch-millisecond deadline for all attempts. | | [options.signal] | AbortSignal | | External cancellation signal. | | [options.allowInsecureBaseURL] | boolean | false | Allow non-loopback HTTP base URLs. | -| [options.onResponse] | function | | Callback receiving provider metadata. | +| [options.onResponse] | function | | Callback receiving successful provider metadata. Its exceptions are ignored. | +| [options.onAttempt] | function | | Callback receiving each completed paid transport attempt as `{ attemptIndex, requestedModel, effectiveModel, usage, retryReason, minimumChargeApplied, outcome }`. Attempt indexes are one-based; its exceptions are ignored. | | [options.sleep] | function | | Injectable sleep function for tests. | | [options.now] | function | | Clock returning epoch milliseconds. | @@ -757,6 +820,26 @@ zero LLM calls and no writes. Language/patterns are resolved per file from the XLIFF target-language (cached), not the global config language. **Kind**: global function + + +## createCancellationController([options]) ⇒ Object +Create a SIGINT-aware cancellation controller for long-running CLI operations. + +**Kind**: global function +**Returns**: Object - Controller facade. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| [options] | object | | Cancellation integration points. | +| [options.processObj] | NodeJS.Process | process | Process-like object used for signal listeners. | +| [options.stderr] | NodeJS.WritableStream | process.stderr | Stream for fallback cancel messages. | +| [options.logger] | object \| null | | Optional patina logger. | + +**Example** +```js +const cancellation = createCancellationController(); +cancellation.install(); +``` ## resolveProfileForLanguage(profileName, lang, [logger]) ⇒ string @@ -775,6 +858,18 @@ Resolve a profile name against language-specific profile limits. ```js resolveProfileForLanguage('namuwiki', 'en') // 'default' ``` + + +## warnIfAlreadyHuman() +Over-editing guard (Study 1 RQ5b): rewriting text that already reads human +measurably nudged it TOWARD AI-likeness (+3.3 judged points on human English +documents, docs/research/2026-rewrite-efficacy-study1.md). When the +deterministic layer finds nothing to fix, say so before spending a rewrite — +advisory only, never blocks, and silent wherever the deterministic score is +unavailable or the text is too short to judge (Study 0 Deviation 1). +Opt out with `over-editing-guard: false` in config. + +**Kind**: global function ## loadConfig([path], [opts]) ⇒ object @@ -966,7 +1061,10 @@ const { frontmatter, body } = splitFrontmatter('---\ntitle: x\n---\nBody'); ## loadPatterns(repoRoot, lang, [skipPatterns]) ⇒ Array.<{file: string, frontmatter: (object\|null), body: string, isStructure: boolean, isScoreOnly: boolean}> -Load language-specific pattern packs from patterns/{lang}-*.md. +Load language-specific pattern packs from patterns/{lang}-*.md, plus any +user or pro packs in custom/patterns/{lang}-*.md. On a filename collision +the custom pack wins (same precedence the persona and lexicon loaders give +custom/), so an installed pack can also override a built-in one. **Kind**: global function **Returns**: Array.<{file: string, frontmatter: (object\|null), body: string, isStructure: boolean, isScoreOnly: boolean}> - Pattern packs. @@ -1106,41 +1204,6 @@ Create a small stderr logger with text and progress modes. const logger = createLogger(); logger.info('event', { message: 'ready' }); ``` - - -## runOuroboros(options) ⇒ Promise.<{finalText: string, finalScore: number, iterations: number, reason: string, log: Array.<object>}> -Run the iterative Ouroboros rewrite-and-score loop. - -**Kind**: global function -**Returns**: Promise.<{finalText: string, finalScore: number, iterations: number, reason: string, log: Array.<object>}> - Final text and iteration log. -**Throws**: - -- Error When model calls or scoring fail outside handled schema fallbacks. - - -| Param | Type | Description | -| --- | --- | --- | -| options | object | Ouroboros options. | -| options.config | object | Effective config with ouroboros settings. | -| options.patterns | Array.<object> | Loaded pattern packs. | -| options.profile | object \| null | Parsed profile. | -| options.voice | object \| null | Parsed voice guide. | -| options.scoring | object \| null | Parsed scoring guide. | -| options.text | string | Source text to improve. | -| [options.apiKey] | string | Provider API key. | -| [options.baseURL] | string | Provider base URL. | -| [options.model] | string | Model id. | -| [options.callLLM] | function | LLM implementation. | -| [options.now] | function | Clock returning epoch milliseconds. | -| [options.sleep] | function | Sleep helper for tests. | -| [options.signal] | AbortSignal | External cancellation signal. | -| [options.timeout] | number | Per-attempt backend timeout in milliseconds. | -| [options.logger] | object | patina logger. | - -**Example** -```js -const result = await runOuroboros({ config, patterns, profile, voice, scoring, text }); -``` ## formatOutput(result, mode, [parsed], [opts]) ⇒ string @@ -1290,6 +1353,24 @@ is a hint surface, not a verdict. ### buildDeterministicAuditBackstop~translationeseRows : Array.<{signal:string, location:string, hint:string}> **Kind**: inner constant of [buildDeterministicAuditBackstop](#buildDeterministicAuditBackstop) + + +## splitPromptForCaching(prompt, [minPrefixChars]) ⇒ Object +Split a built prompt into a cacheable static prefix and a dynamic tail for +provider prompt caching. The prefix is everything before the FIRST input +fence: on a first-turn prompt that is the full static catalog (identical +across requests for a given lang/profile/persona), while refine prompts +carry variable fenced references near the top, so their prefix falls under +the minimum and caching is skipped — avoiding cache writes that would never +be re-read. + +**Kind**: global function + +| Param | Type | Description | +| --- | --- | --- | +| prompt | string | Built prompt text. | +| [minPrefixChars] | number | Minimum prefix size worth caching (~1k tokens). | + ## fenceReferenceText(text, [options]) ⇒ string @@ -1312,7 +1393,7 @@ own output is unchanged. Resolve the effective per-detection severity points for a config. Single resolution path for every prompt surface: yaml -`ouroboros.severity-points` overrides the documented defaults key-by-key. +`scoring.severity-points` overrides the documented defaults key-by-key. **Kind**: global function **Returns**: Object - Effective severity points. @@ -1440,6 +1521,24 @@ Resolve effective API key, base URL, and model from explicit values, provider, a ```js const resolved = resolveProviderConfig({ provider: selectProvider('openai') }); ``` + + +## validateAttemptRecord(value) +**Kind**: global function + +| Param | Type | +| --- | --- | +| value | unknown | + + + +## notifyInvalidAttempt(onAttemptInvalid) +**Kind**: global function + +| Param | Type | +| --- | --- | +| onAttemptInvalid | function \| undefined | + ## scoreText(options) ⇒ Promise.<object> @@ -1469,6 +1568,9 @@ Score text for AI-likeness using an LLM JSON scorer plus deterministic shadow si | [options.now] | function | Clock returning epoch milliseconds. | | [options.sleep] | function | Sleep helper for tests. | | [options.responseFormat] | object | Opt-in OpenAI-compatible structured-output request field forwarded to callLLM. | +| [options.extraBody] | object | Opt-in provider-specific request fields (e.g. reasoning control) spread into the request body. | +| [options.onAttempt] | function | Safe callback for one-based paid-attempt metadata records. | +| [options.onAttemptInvalid] | function | Safe callback when transport evidence is malformed; receives no provider metadata. | **Example** ```js @@ -1487,6 +1589,7 @@ Compute deterministic stylometry/lexicon AI-likeness signals. | [options] | object | | Deterministic scoring options. | | [options.text] | string | | Text to analyze. | | [options.config] | object | {} | Effective config. | +| [options.patterns] | Array | [] | Loaded pattern packs; used for short-form category math. | | [options.repoRoot] | string | | Repository root for analyzer resources. | | [options.logger] | object | | Optional logger for recoverable deterministic warnings. | | [options.analyzer] | function | | Analyzer implementation. | @@ -1563,6 +1666,9 @@ Score meaning preservation between original and rewritten text. | [options.now] | function | Clock returning epoch milliseconds. | | [options.sleep] | function | Sleep helper for tests. | | [options.responseFormat] | object | Opt-in OpenAI-compatible structured-output request field forwarded to callLLM. | +| [options.extraBody] | object | Opt-in provider-specific request fields (e.g. reasoning control) spread into the request body. | +| [options.onAttempt] | function | Safe callback for one-based paid-attempt metadata records. | +| [options.onAttemptInvalid] | function | Safe callback when transport evidence is malformed; receives no provider metadata. | **Example** ```js @@ -1629,6 +1735,9 @@ Score fidelity between original and rewritten text using length plus LLM criteri | [options.now] | function | Clock returning epoch milliseconds. | | [options.sleep] | function | Sleep helper for tests. | | [options.responseFormat] | object | Opt-in OpenAI-compatible structured-output request field forwarded to callLLM. | +| [options.extraBody] | object | Opt-in provider-specific request fields (e.g. reasoning control) spread into the request body. | +| [options.onAttempt] | function | Safe callback for one-based paid-attempt metadata records. | +| [options.onAttemptInvalid] | function | Safe callback when transport evidence is malformed; receives no provider metadata. | **Example** ```js diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b74a7ca..63d3c2e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -141,7 +141,6 @@ option, not a standalone mode. benchmark/gate layer) - `src/verify.js` — post-rewrite meaning verification + one strict retry (`deterministicMeaningGuard` is its LLM-free part) -- `src/ouroboros.js` — iterative multi-pass rewrite with regression rollback - `src/web-rewrite.js`, `web-rewrite-contract.js`, `web-rewrite-stream.js`, `rewrite-handler.js`, `streaming-api.js` — web / hosted rewrite path - `src/web-config.js`, `web-observability.js`, `rate-limit.js`, `security.js` — @@ -157,7 +156,13 @@ option, not a standalone mode. kept as shared transport) - `src/auth.js`, `commands/auth.js`, `commands/doctor.js` - `src/ocr.js` — image → text input extraction +- `scoring` and `verification` are separate configuration namespaces; persona + thresholds remain in persona definitions. +### Packaged research comparator (unsupported) + +- `scripts/iterative-rewrite-baseline.mjs` — `iterative-baseline`, a packaged + research comparator outside the product API, CLI help, and configuration surface. The package has no `exports` map, so the module remains deep-importable but unsupported. --- ## Seams: resolved and remaining @@ -216,11 +221,6 @@ of them; what remains is named here as the surface for later work. proving ~0 false positives on legitimate rewrites (live-quality + dogfood + the local calibration corpus) and true positives on the meaning-broken fixtures, recording `source: calibrated`. -7. **`ouroboros.js` persona drift — won't-do.** The iterative loop is a - research-only A/B baseline (`scripts/rewrite-ab.mjs`; the `--ouroboros` CLI - flag was removed and `--verify` replaced it). The live path (`--verify` + the - always-on persona gate) already runs `persona-match`, so wiring it into a - research-only loop is not worth it. ### Remaining diff --git a/docs/CLI.md b/docs/CLI.md index 20a4bf3..7b38e42 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -49,7 +49,7 @@ patina --lang en --score --exit-on 30 draft.md ## Meaning verification: `--verify` -`--verify` folds a meaning-preservation check into the normal rewrite. After the rewrite it scores MPS and fidelity; if either is below the floor (`ouroboros.mps-floor` / `ouroboros.fidelity-floor`, default 70) it runs **one** conservative retry that re-rewrites from the original with a strict meaning-preservation directive. If the retry still misses, patina emits the closest (highest-fidelity) candidate and warns on stderr — fail-closed but non-destructive, so stdout always carries usable text. +`--verify` folds a meaning-preservation check into the normal rewrite. After the rewrite it scores MPS and fidelity; if either is below the floor (`verification.{mps-floor,fidelity-floor}`, default 70) it runs **one** conservative retry that re-rewrites from the original with a strict meaning-preservation directive. If the retry still misses, patina emits the closest (highest-fidelity) candidate and warns on stderr — fail-closed but non-destructive, so stdout always carries usable text. ```bash patina --verify draft.md @@ -58,7 +58,9 @@ patina --verify --lang ko --backend codex-cli draft.md - It is a rewrite modifier, not a separate mode: combining it with `--score`, `--audit`, `--diff`, or `--preview` is an input error (those do not rewrite). - The MPS/fidelity scorers run through the **selected backend**, so `--verify` works with HTTP and local CLI backends alike. It adds up to four extra model calls (two scorers, plus a retry that re-scores), so the plain rewrite stays the fast/cheap default. -- `--ouroboros` was **removed**. The iterative loop is gone; `--verify` is its meaning-floor replacement. (The multi-pass loop survives only as a research baseline in `npm run quality:rewrite-ab`.) +The Node CLI keeps `scoring` and `verification` separate; `--verify` uses +`verification.{mps-floor,fidelity-floor}`, while persona thresholds remain in +persona definitions. ### Deterministic meaning guard (always on, no LLM) @@ -148,12 +150,8 @@ In every depth, facts, numbers, names, and causal claims must never be invented, ## Stderr logs -Human-facing status, warnings, and progress indicators go to stderr so stdout -stays reserved for the transformed text or JSON envelope. - -- `--quiet` suppresses stderr logs, including Ouroboros progress. -- Ouroboros reports per-iteration score movement and latency. - +Human-facing status and warnings go to stderr so stdout stays reserved for the +transformed text or JSON envelope. `--quiet` suppresses those stderr logs. ## In-place preview: `--preview` diff --git a/docs/COOKBOOK.md b/docs/COOKBOOK.md index ef34af6..f34adf2 100644 --- a/docs/COOKBOOK.md +++ b/docs/COOKBOOK.md @@ -2,7 +2,7 @@ Practical recipes for plugging patina into existing writing and CI workflows. Each recipe is self-contained — copy, adapt, run. -For the full flag list see `patina --help` and [`CLI.md`](CLI.md). For tone / profile background see [`README.md`](../README.md#modes). +For the full flag list see `patina --help` and [`CLI.md`](CLI.md). For persona, tone, and profile background see [`README.md`](../README.md#modes). --- @@ -98,15 +98,17 @@ skip-patterns: --- -## 5. Create a custom profile (copy `blog.md`, edit voice-overrides) +## 5. Create a custom profile (copy `blog.md`, edit pattern policy) -When the built-in 12 profiles don't match your house style, fork the closest one: +Use a custom profile to apply deterministic pattern policy for a house style. Persona is the reusable voice-composition control; tone is the Korean/English `casual` / `professional` / `auto` register override and wins over a persona's register. A profile is not the place to compose a new persona. + +When the built-in profiles do not match your local pattern policy, fork the closest one: ```bash cp profiles/blog.md profiles/my-newsletter.md ``` -Edit the frontmatter — at minimum change `profile:`, then tune `voice-overrides` and `pattern-overrides` to match the voice you want: +Edit the frontmatter — at minimum change `profile:`, then tune `pattern-overrides` for the policy you need: ```yaml --- @@ -114,11 +116,6 @@ profile: my-newsletter # must match the filename without .md name: Internal newsletter profile version: 1.0.0 scope: weekly engineering newsletter -voice-overrides: - first-person: amplify # we sign every post - opinions: amplify # opinionated framing is the point - humor: allow # dry humor ok - messiness: reduce # cleaner than personal blog pattern-overrides: en: 14: suppress # bold is allowed for scannable sections @@ -126,15 +123,17 @@ pattern-overrides: --- ``` -Then opt-in per run: +Then opt in per run: ```bash patina --lang en --profile my-newsletter post.md ``` -Voice-override values are `amplify` / `allow` / `reduce` / `suppress`; pattern IDs and their meanings are in [`PATTERNS.md`](PATTERNS.md). +Pattern-policy values are `amplify` / `allow` / `reduce` / `suppress`; pattern IDs and their meanings are in [`PATTERNS.md`](PATTERNS.md). -> **What actually runs:** a `pattern-overrides` entry set to **`suppress`** is applied deterministically — patina drops that pattern from the rewrite / audit / score prompt for the profile's language, so the model never flags it (e.g. `legal` suppresses Korean passive-voice #27). `reduce` / `amplify` are **advisory** for now: they document intent and are reinforced by the profile's prose body, but the engine does not yet adjust their weight. +> **What actually runs:** a `pattern-overrides` entry set to **`suppress`** is applied deterministically — patina drops that pattern from the rewrite / audit / score prompt for the profile's language, so the model never flags it (e.g. `legal` suppresses Korean passive-voice #27). `reduce` / `amplify` are advisory for now: they document intent, but the engine does not yet adjust their weight. +> +> **Current vs. target:** profiles currently supply deterministic pattern policy, but some skill and core-prompt paths still carry legacy voice guidance. “Profile is pattern-policy only everywhere” is therefore a target, not a universal current invariant. This release changes no prompts or runtime behavior. --- diff --git a/docs/EXIT-CODES.md b/docs/EXIT-CODES.md index b06b33d..2f29339 100644 --- a/docs/EXIT-CODES.md +++ b/docs/EXIT-CODES.md @@ -25,8 +25,8 @@ meaning-and-facts signals. Any one of them failing exits `4`: | Signal | Fails when | |---|---| -| `mps` | meaning-preservation score below the floor (`ouroboros.mps-floor`, default 70) | -| `fidelity` | fidelity score below the floor (`ouroboros.fidelity-floor`, default 70) | +| `mps` | meaning-preservation score below `verification.mps-floor` (default 70) | +| `fidelity` | fidelity score below `verification.fidelity-floor` (default 70) | | `numbers` | a number present in the source is missing from the rewrite | Exit `4` is **enforcing but non-destructive**: patina prints the rewrite anyway diff --git a/docs/FAQ.md b/docs/FAQ.md index 3e34358..649aa3a 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -64,9 +64,11 @@ No. patina runs as a skill for Claude Code, Codex CLI, Cursor, and OpenCode, and Korean, English, Chinese, and Japanese are supported. Pattern packs are auto-discovered by language prefix, so new languages can be added by contributing new pattern files. -## Can I add my own writing style or patterns? +## Do persona, profile, and tone overlap? -Yes. Use custom profiles for voice preferences and custom pattern packs for local rules. The repo keeps built-in patterns separate from user customizations. +They serve different jobs. A persona owns reusable voice composition. In Korean and English, `--tone casual|professional|auto` is the register override and takes precedence over a persona's register. A profile supplies deterministic pattern policy; use a custom pattern pack for separate local rules. + +That separation is the current direction, not a claim that every path has reached the target: some skill and core-prompt paths still carry legacy voice guidance from profiles. This release does not change prompts or runtime behavior, merge the axes, rename `--tone`, or expose new hosted controls. Completing profile-policy-only handling everywhere, verifying retry-context preservation, and considering a user-facing “Register” name are later work. ## What should contributors start with? diff --git a/docs/FAQ_KR.md b/docs/FAQ_KR.md index 02b1459..2736910 100644 --- a/docs/FAQ_KR.md +++ b/docs/FAQ_KR.md @@ -56,9 +56,11 @@ score는 0부터 100까지의 대략적인 편집 신호입니다. 낮을수록 한국어, 영어, 중국어, 일본어를 지원합니다. 패턴 팩은 언어 접두사로 자동 탐색되므로 새 언어는 새 패턴 파일을 기여해 추가할 수 있습니다. -## 내 문체나 패턴을 추가할 수 있나요? +## persona, profile, tone은 서로 겹치나요? -네. voice preference에는 custom profile을, 로컬 규칙에는 custom pattern pack을 사용하세요. repo는 built-in pattern과 사용자 custom 설정을 분리합니다. +역할이 다릅니다. persona는 재사용 가능한 voice composition을 맡습니다. 한국어와 영어에서 `--tone casual|professional|auto`는 register override이며 persona의 register보다 우선합니다. profile은 deterministic pattern policy를 제공하며, 별도의 로컬 규칙에는 custom pattern pack을 사용하세요. + +이 구분은 현재 방향이지 모든 경로가 이미 목표에 도달했다는 뜻은 아닙니다. 일부 skill 및 core-prompt 경로에는 profile의 legacy voice guidance가 아직 남아 있습니다. 이번 릴리스는 prompt나 runtime behavior를 바꾸지 않으며, 축을 합치거나 `--tone`을 이름 변경하거나 새 hosted control을 노출하지 않습니다. 모든 경로에서 profile을 pattern policy로만 완성하는 일, retry context preservation 검증, 사용자 대상 “Register” 명칭 검토는 이후 작업입니다. ## 기여자는 무엇부터 시작하면 좋나요? diff --git a/docs/FLAG-PARITY.md b/docs/FLAG-PARITY.md index 5211be5..7dde922 100644 --- a/docs/FLAG-PARITY.md +++ b/docs/FLAG-PARITY.md @@ -9,8 +9,8 @@ Basis: local checkout plus `node bin/patina.js --help` and `SKILL.md` reviewed o | `--audit` | ✓ | ✓ | Detection-only mode. | | `--score` | ✓ | ✓ | Score mode is available on both surfaces. | | `--exit-on ` | ✓ | — | CLI score-gate spelling for CI. | -| `--verify` | ✓ | — | Rewrite + MPS/fidelity meaning-floor check with one retry (replaces the old loop). | -| `--ouroboros` | — | ✓ | Removed from the CLI; the `/patina` skill still runs its own iterative loop. | +| `--verify` | ✓ | — | Node CLI rewrite + MPS/fidelity meaning-floor check with one retry. | +| `--strict` | — | ✓ | Agent-skill-only strict rewrite flow. | | `--format ` | ✓ | — | CLI output-envelope feature. | | `--quiet` | ✓ | — | CLI stderr log suppression for scripts. | | `--batch` | ✓ | ✓ | Multi-file CLI/skill rewrite flow. | diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 01192c0..b0b472f 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -60,13 +60,6 @@ survive the rewrite pipeline and whether polarity is preserved. See [the FAQ](FAQ.md#what-is-mps) and [MPS scoring](../core/scoring.md#16-mps-scoring-formula). -## Ouroboros loop - -An iterative rewrite loop that keeps trying to lower AI-likeness while obeying -meaning-preservation gates. The standalone CLI replaced it with `--verify` -(rewrite + meaning-floor retry); the loop still lives in the `/patina` skill -and the `quality:rewrite-ab` research baseline. See -[Ouroboros Termination](../core/scoring.md#ouroboros-termination). ## Pattern diff --git a/docs/HARNESS.md b/docs/HARNESS.md index e459380..5ca9cbb 100644 --- a/docs/HARNESS.md +++ b/docs/HARNESS.md @@ -50,7 +50,7 @@ INPUT ENGINE (deterministic, LLM-free) SURFACES Hot decision = OR of the per-paragraph signals (`burstiness_low`, `mattr_low`, `lexicon_hot`, `ko_diagnostics`, `candor`, `thematic_break`, `ko_ending_monotony`) plus the document-level `markup_leakage` / `structural_model`. The signal-impact -harness below ablates each one to report its marginal contribution. +analysis below ablates each one to report its marginal contribution. ## Quality & regression (deterministic, CI-safe) @@ -82,7 +82,7 @@ harness below ablates each one to report its marginal contribution. |---|---|---|---| | Live rewrite quality | `npm run quality:live` (`PATINA_LIVE=1` to call a model) | before/after AI score, MPS, fidelity on rewrites | [tests/quality/README.md](../tests/quality/README.md) | | Adversarial MPS | `npm run quality:adversarial-mps` | Guards against MPS hiding unchanged AI style | [tests/quality/README.md](../tests/quality/README.md) | -| Rewrite A/B | `npm run quality:rewrite-ab` (`--live`) | Compares two rewrite configs (default `single` vs `ouroboros` multi-pass) on the same fixtures: after-AI/MPS/fidelity/edit-churn + per-fixture winner. Answers "does the multi-pass pipeline rewrite better?" | [tests/quality/README.md](../tests/quality/README.md) | +| Rewrite A/B | `npm run quality:rewrite-ab` (`--live`) | Compares two rewrite configs (default `single` vs `iterative-baseline`) on the same fixtures: after-AI/MPS/fidelity/edit-churn + per-fixture winner. Answers whether the baseline comparison flow produces a better rewrite. | [tests/quality/README.md](../tests/quality/README.md) | ## Gates (deterministic, run in CI / pre-publish) diff --git a/docs/PATTERNS.md b/docs/PATTERNS.md index 3e63314..abd2fda 100644 --- a/docs/PATTERNS.md +++ b/docs/PATTERNS.md @@ -11,7 +11,7 @@ Patina ships 184 pattern entries across four languages. The language-specific re ## Notes -- Rewrite-capable patterns are applied by the rewrite modes (default rewrite, `--verify`, and the skill's `--ouroboros` loop) and `--diff`, according to their pack metadata and runtime mode. +- Rewrite-capable patterns are applied by the rewrite modes (default rewrite, `--verify`, and the skill-only `/patina --strict` multi-pass flow) and `--diff`, according to their pack metadata and runtime mode. - Viral-hook patterns are score/audit-only SNS-marketing signals. They affect `--score` and `--audit`, but rewrite modes skip them because the rhetoric may be intentional. - Pattern packs are auto-discovered from `patterns/{lang}-*.md`. To add a language or custom pack, follow [CONTRIBUTING.md](../CONTRIBUTING.md) and the frontmatter format used in the existing packs. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 245d316..d243b8f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -186,12 +186,21 @@ Acceptance criteria: - Integration docs are tested manually before public launch posts. - Each integration has one minimal example and one realistic example. -### Phase 4 — custom voice authoring +### Phase 4 — custom persona authoring -Goal: let users define their own persona / genre / tone instead of supplying raw style samples. +**Voice-axis baseline (release 7.0.0 documentation):** persona owns reusable voice composition. In Korean and English, `--tone casual|professional|auto` is the register override and wins over a persona's register. A profile currently supplies deterministic pattern policy. -- Build on the existing persona harness (`src/personas/`, `personas/ko/`) and the `--persona` / `--tone` / `--profile` axes rather than a separate sample-injection path. -- Provide an authoring entry point so a user can create and reuse a named custom persona (voice + register + genre) with the same MPS/fidelity floors enforced. +This is a current-vs-target distinction: some skill and core-prompt paths still carry legacy voice guidance from profiles, so “profile is pattern-policy only everywhere” is not yet a universal invariant. This release changes no prompts or runtime behavior, does not merge the axes or rename `--tone`, and exposes no new hosted controls. + +Later work, not shipped behavior: +- Complete profile-policy-only handling across the remaining paths. +- Verify retry-context preservation when persona, tone, and profile are combined. +- Consider whether user-facing copy should call the register control “Register” while retaining `--tone`. + +Goal: let users define and reuse a persona rather than supplying raw style samples. + +- Build on the existing persona harness (`src/personas/`, `personas/ko/`) and the separate `--persona` / `--tone` / `--profile` axes rather than a sample-injection path. +- Provide an authoring entry point so a user can create and reuse a named custom persona for voice composition and genre; tone remains the run-level register override, with the same MPS/fidelity floors enforced. - This replaces the removed `--voice-sample` style anchor (dropped in 6.0.0): the "sound like me" use case becomes a saved custom persona, not a per-run sample file. - **Corpus-distilled quantitative bands**: a `persona new --from-corpus ` path that ingests a multi-document personal corpus and distills per-metric allow-bands (p5/p95) from patina's existing deterministic stylometry (burstiness / MATTR / lexicon density / line rhythm) — promoting a persona from qualitative blocks to a quantitative, verifiable voice fingerprint, optionally segmented by genre/channel. Bands live in `src/features/*` (LLM-free); the persona still cannot lower the MPS/fidelity floors. - **Personalized avoided-lexicon**: derive a persona's `avoid` list from the user corpus's zero-occurrence terms (a personal AI-tell dictionary), complementing the generic corpus-grounded AI lexicon. @@ -199,7 +208,6 @@ Goal: let users define their own persona / genre / tone instead of supplying raw - Trigger + attribution: a large effort that competes with the payment/launch path — implementation starts only after payment stabilization and a **separate** approval. The genre×channel fingerprint bands, personalized zero-occurrence tells, and holdout/ledger methodology are adopted (idea-level) from `kimsh-1/gn-voice` (MIT — Section A: `scripts/`, `references/fingerprint-slim.json`, `references/ai-tells.json`, `style-profile/`). patina distills only the user's own corpus and never ingests gn-voice's `corpus/`, `analysis/`, or `examples/` (Section B, all rights reserved). Credit gn-voice in `NOTICE` if any Section A structure is reused. Acceptance criteria: - - A user can author, save, and select a custom persona without editing source. - Custom personas honor the same meaning-preservation/fidelity hard floors as bundled personas. - No regression to the conservative `preserve` default for users who do not author one. diff --git a/docs/agents.md b/docs/agents.md index 1f2fc1b..b37d838 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -20,7 +20,7 @@ Use when: you need a pre-rewrite audit log, want to verify which patterns were a Given the ORIGINAL and REWRITE texts, audits meaning preservation against all four fidelity criteria defined in `core/scoring.md` §§9-14: claims preserved, no fabrication, tone match, and length ratio. Also checks MPS-level semantic anchors: numbers, polarity, causation, named entities, and direct quotes. Returns a PASS or NEEDS-ROLLBACK verdict with the fidelity score and offending spans identified. -The ouroboros floor applies here: fidelity_score ≥ 70 is required for PASS. +The verification floor applies here: fidelity_score ≥ `verification.fidelity-floor` (default: 70) is required for PASS. Use when: you need an auditable record that the rewrite did not alter facts, or when fidelity is critical (academic, technical, medical, legal profiles). @@ -35,7 +35,7 @@ Re-runs detection on the **rewrite text** to check for residual AI tells. Comput | C | Residual score 51–70 or moderate over-editing — retry recommended | | D | Residual score > 70 or genre/register violation — rollback required | -Use when: you want quality assurance on the rewrite before delivering it, or when the `--ouroboros` loop needs an independent grade signal. +Use when: you want quality assurance on the rewrite before delivering it, or when the strict flow needs an independent grade signal. ## Strict Flow: Composition @@ -71,7 +71,7 @@ The `/patina --strict` mode (defined in `SKILL.md`) wires this flow automaticall ## Advisory Metadata Rule -Korean `translationese` and `koPostEditese.v1` signals are **advisory only** across the entire pipeline, including inside all three subagents. These signals must never influence the AI-likeness score, fidelity score, quality grade, ouroboros termination, or any authorship verdict. They appear in report output labeled as advisory and non-scoring. +Korean `translationese` and `koPostEditese.v1` signals are **advisory only** across the entire pipeline, including inside all three subagents. These signals must never influence the AI-likeness score, fidelity score, quality grade, verification outcome, or any authorship verdict. They appear in report output labeled as advisory and non-scoring. ## Claude Code Plugin Auto-Discovery diff --git a/docs/research/adversarial-mps.md b/docs/research/adversarial-mps.md index aa6d1e7..24ee9ea 100644 --- a/docs/research/adversarial-mps.md +++ b/docs/research/adversarial-mps.md @@ -37,5 +37,5 @@ Keep MPS unchanged for semantic safety, then add an independent recurrence gate: 1. Score the original and rewritten text with deterministic `analyzeText`. 2. If `MPS ≥ 90` and rewritten AI score remains `≥ 60`, mark the candidate as `style_not_improved`. -3. In Ouroboros selection, prefer candidates that pass MPS and lower the AI score; do not let high MPS alone rescue a visibly AI-like rewrite. +3. In iterative-baseline selection, prefer candidates that pass MPS and lower the AI score; do not let high MPS alone rescue a visibly AI-like rewrite. 4. Report preserved anchors and recurring AI markers separately so users can decide whether to edit more or keep the register. diff --git a/docs/social/patina-launch-korean-first.md b/docs/social/patina-launch-korean-first.md index eae58dd..0a5571f 100644 --- a/docs/social/patina-launch-korean-first.md +++ b/docs/social/patina-launch-korean-first.md @@ -21,7 +21,7 @@ notes: 의미보존 게이트가 라이브에서 실제로 일했습니다. 홍보 문구를 넣었더니 모델이 원문에 없던 "업무 시간 30% 절감" 통계를 지어냈는데, fidelity 점수가 바닥나면서 출력이 통째로 거부됐습니다. 문장은 바꿔도 주장과 숫자는 못 바꾼다는 원칙이 지어낸 통계 앞에서 작동한 사례라 공유합니다. -플레이그라운드 첫 화면이 이제 브라우저 언어를 따라갑니다. 그동안 무조건 한국어로 떴는데, 영어 브라우저면 영어로 시작합니다. +플레이그라운드 첫 화면은 이제 브라우저 설정과 관계없이 영어로 일관되게 시작합니다. 한국어·중국어·일본어 글을 붙여넣으면 첫 전송 때 언어를 자동 감지해 전환합니다. 오탐 신고 링크도 다시 달았습니다. 개편 과정에서 빠져 있었는데, 이제 리라이트 결과에 탐지 신호가 뜨면 걸린 문장이 채워진 깃허브 폼으로 바로 연결됩니다. 본문에 쓴 대로, 사람이 쓴 글을 AI로 잡는 사례 제보가 제일 큰 기여입니다. diff --git a/package-lock.json b/package-lock.json index f5799b1..3a7da58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "patina-cli", - "version": "6.3.4", + "version": "7.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "patina-cli", - "version": "6.3.4", + "version": "7.0.0", "license": "MIT", "dependencies": { "js-yaml": "^4.1.0" diff --git a/package.json b/package.json index 1facf1d..8d818bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "patina-cli", - "version": "6.3.4", + "version": "7.0.0", "description": "AI text humanizer CLI — detects and removes AI writing patterns", "type": "module", "main": "src/cli.js", @@ -31,7 +31,7 @@ "badge": "node scripts/badge-json.mjs", "card": "node scripts/share-card.mjs", "lint": "npm run lint:syntax && npm run lint:eslint && npm run typecheck && npm run spellcheck", - "release:check": "node scripts/check-release-metadata.mjs", + "release:check": "node scripts/check-release-metadata.mjs && node scripts/check-retired-concepts.mjs", "release:ready": "node scripts/check-v6.4-release-ready.mjs", "check:no-private-assets": "node scripts/check-no-private-assets.mjs", "prepublishOnly": "RELEASE_READY_CONTEXT=publish npm run release:ready && npm run release:check && npm run check:no-private-assets && npm test && npm run benchmark:report && npm run dogfood", @@ -145,6 +145,8 @@ "scripts/rebaseline-score.mjs", "scripts/signal-impact.mjs", "scripts/rewrite-ab.mjs", + "scripts/iterative-rewrite-baseline.mjs", + "scripts/check-retired-concepts.mjs", "tests/quality/adversarial-mps/", "tests/quality/rebaseline-manifest.example.jsonl", "artifacts/rebaseline-2025/README.md", diff --git a/packages/patina-humanizer/package.json b/packages/patina-humanizer/package.json index 853a8b4..6ca739f 100644 --- a/packages/patina-humanizer/package.json +++ b/packages/patina-humanizer/package.json @@ -1,13 +1,13 @@ { "name": "patina-humanizer", - "version": "6.3.4", + "version": "7.0.0", "description": "SEO-friendly alias for patina-cli", "type": "module", "bin": { "patina-humanizer": "bin/patina-humanizer.js" }, "dependencies": { - "patina-cli": "6.3.4" + "patina-cli": "7.0.0" }, "license": "MIT", "repository": { diff --git a/playground/chatgpt.js b/playground/chatgpt.js index 81da6d0..62edd79 100644 --- a/playground/chatgpt.js +++ b/playground/chatgpt.js @@ -16,7 +16,7 @@ import { // Browser globals (eslint config declares only Node globals; sibling modules use // the same globalThis convention — e.g. rewrite-client.js). -const { document, Option, navigator } = globalThis; +const { document, Option } = globalThis; const $ = (sel) => /** @type {HTMLElement} */ (document.querySelector(sel)); const els = { @@ -819,9 +819,9 @@ function buildOutputActions(text) { return actions; } -// Auto-detect the dominant script so pasted EN/ZH/JA text is not silently -// rewritten under the default (ko) language. Kana => ja; Hangul => ko; Han -// without kana => zh; Latin => en. Returns null when undecidable. +// Auto-detect the dominant script so pasted KO/EN/ZH/JA text is not silently +// rewritten under the wrong language. Kana => ja; Hangul => ko; Han without +// kana => zh; Latin => en. Returns null when undecidable. function detectLang(text) { const s = String(text || ''); if (/[\u3040-\u30ff]/.test(s)) return 'ja'; @@ -831,20 +831,10 @@ function detectLang(text) { return null; } -// Pick the initial UI language from the browser locale (ko/en/zh/ja; default -// en). No client-side storage — the playground persists nothing; an explicit -// pick simply lives in the select for the rest of the session, and pasted text -// still re-routes via detectLang on the first turn. -function initialLang() { - const langs = (Array.isArray(navigator.languages) && navigator.languages.length) - ? navigator.languages - : [navigator.language]; - for (const l of langs) { - const base = String(l || '').toLowerCase().slice(0, 2); - if (Object.hasOwn(I18N, base)) return base; - } - return 'en'; -} +// Always start the public playground in English. An explicit language pick lives +// in the select for the rest of the session, while pasted text still re-routes +// via detectLang on the first turn. +const DEFAULT_LANG = 'en'; // ---------- unified submit ---------- /** In-flight rewrite attempt: { controller, cancelled }. One at a time (busy gate). */ @@ -939,9 +929,9 @@ async function submit(text, source = 'hero') { if (!convo) { newConvo(); convo = activeConvo(); } if (!convo) return; - // Match the language to the pasted text's script on the first turn (the - // selector defaults to ko; without this, EN/ZH/JA input is silently rewritten - // under the wrong language). Refine turns keep the conversation's language. + // Match the language to the pasted text's script on the first turn so non-English + // input is not silently rewritten under the English default. Refine turns keep + // the conversation's language. const detected = convo.thread.original == null ? detectLang(clean) : null; if (detected && detected !== els.lang.value) { els.lang.value = detected; @@ -1257,7 +1247,7 @@ els.licenseKey.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.p els.provider.addEventListener('change', populateModels); // ---------- init ---------- -els.lang.value = initialLang(); +els.lang.value = DEFAULT_LANG; populateProviders(); syncTier(); renderSuggest(); diff --git a/playground/index.html b/playground/index.html index 2fee68b..c3aa385 100644 --- a/playground/index.html +++ b/playground/index.html @@ -220,7 +220,7 @@

Paste your own and see