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/.env.example b/.env.example
index 13c41ae..3d71647 100644
--- a/.env.example
+++ b/.env.example
@@ -44,17 +44,20 @@
# PATINA_FREE_MODEL=gemini-3.6-flash
# PATINA_QUOTA_HMAC_SECRET=your-long-random-secret
-# --- Pro tier ($9.99/mo USD, Lemon Squeezy license-gated) -------------------
+# --- Pro tier ($9.99/mo USD, license-gated; production provider: Polar) -----
# Caller sends `Authorization: Bearer `; the server validates it
-# against Lemon Squeezy's validate-only endpoint (POST /v1/licenses/validate),
-# never stores the raw key, and meters per-license by an HMAC subject.
+# against the provider's validate-only endpoint (Polar customer-portal
+# license-keys/validate in production), never stores the raw key, and meters
+# per-license by an HMAC subject.
# --- License provider ------------------------------------------------------
-# Which vendor gates the Pro tier. Defaults to lemonsqueezy so an existing
-# deployment cannot change gate behavior by upgrading. Both providers share the
-# same fail-closed security core; cache and lock keys are provider-namespaced,
-# so switching never serves a decision cached under the other vendor.
-# PATINA_LICENSE_PROVIDER=lemonsqueezy # or: polar
+# Which vendor gates the Pro tier. The code default stays lemonsqueezy so an
+# existing deployment cannot change gate behavior by upgrading, but Lemon
+# Squeezy declined the account (docs/operations/payment-provider-reset-20260729.md)
+# — production runs polar. Both providers share the same fail-closed security
+# core; cache and lock keys are provider-namespaced, so switching never serves
+# a decision cached under the other vendor.
+# PATINA_LICENSE_PROVIDER=polar # production value; code default: lemonsqueezy
#
# Polar identity (PATINA_LICENSE_PROVIDER=polar). Both are REQUIRED — a missing
# value fails closed rather than skipping the corresponding check.
@@ -69,7 +72,8 @@
# # seat validates about once per cache TTL.
# PATINA_POLAR_TIMEOUT_MS / _CACHE_TTL_MS / _NEGATIVE_CACHE_TTL_MS / _LOCK_WAIT_MS
# # same meaning as the PATINA_LS_* pair below
-# Lemon Squeezy identity — the validate response meta MUST match these.
+# Lemon Squeezy identity (retired provider, kept for the code path) — the
+# validate response meta MUST match these.
# LS_STORE_ID=425473 # required
# LS_PRO_VARIANT_ID=1875389 # required (Pro subscription variant)
# LS_PRO_PRODUCT_ID=1199625 # optional extra pin (meta.product_id)
@@ -163,9 +167,9 @@
#
# Enabled checkout additionally requires an exact source-controlled binding in
# scripts/checkout-evidence-bindings.mjs. Environment values never authorize a
-# checkout URL. Adding a binding requires reviewed immutable PAY-STG/PAY-B
-# evidence and a human-supplied exact Lemon Squeezy URL; keep checkout disabled
-# until that review is complete.
+# checkout URL. Adding a binding requires reviewed immutable PAY-B evidence and
+# a human-supplied exact Polar checkout-link URL (https://buy.polar.sh/polar_cl_*);
+# keep checkout disabled until that review is complete.
# PATINA_DEPLOYMENT_CHANNEL=staging
# PATINA_PRO_CHECKOUT_URL=
# PATINA_PRO_GATE_EVIDENCE_ID=
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..e28a811 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,69 @@ 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.
```
+## Unreleased
+
+### Changed
+
+- The v6.4 preflight hold now certifies the Polar payment route instead of the
+ declined Lemon Squeezy account: the checkout evidence binding table carries
+ exactly the Polar production tuple (`PAY-B-20260729-POLAR-ea8385dc-4c9c3f17`,
+ `https://buy.polar.sh/polar_cl_…`), the hold validates the
+ `PAY-B-BINDING-POLAR-v1` and `PAY-LIVE-RUNTIME-POLAR-v1` artifacts,
+ `POLAR_APPROVAL` replaces `LS_APPROVAL`, and the secret-manager blocker names
+ `PATINA_LICENSE_PROVIDER`/`POLAR_ORGANIZATION_ID`/`POLAR_PRO_BENEFIT_ID`. The
+ retired staging chain is recorded as superseded by the production zero-amount
+ purchase evidence, not silently dropped; Lemon Squeezy records stay on disk,
+ hash-frozen, as history. Checkout remains disabled pending the owner's
+ env-side enable sequence.
+
+## 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.
+
+### Added
+
+- `patina --score --offline` runs the deterministic scoring layer without resolving a backend or credential. It supports markdown/JSON output and `--exit-on`; LLM-judged categories are marked unavailable, and a missing numeric local score fails instead of silently passing.
+
+### Fixed
+
+- Rewrite stdout now contains prose only. Plain, fenced, and blockquoted tone footers are stripped, while `--format json` retains structured tone metadata, including the model-resolved value from `--tone auto`.
+- The agent skill now uses the CLI's config precedence and canonical project filename: installed `.patina.default.yaml` → `~/.patina.yaml` → project `./.patina.yaml` → command arguments.
+
+### 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..2879149 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
-
+
@@ -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 |
@@ -127,8 +127,9 @@ patina --lang [mode] [--profile ] input.txt
|---|---|
| `patina input.txt` | rewrite with defaults |
| `patina --audit input.txt` | detect patterns only |
-| `patina --score input.txt` | output a 0-100 AI-likeness score |
-| `patina --score --exit-on 30 input.txt` | CI gate with exit code `3` when `overall > 30` |
+| `patina --score input.txt` | output a 0-100 score using the LLM judge plus deterministic signals |
+| `patina --score --offline input.txt` | score with deterministic signals only; no backend or API key |
+| `patina --score --offline --exit-on 30 input.txt` | offline CI gate with exit code `3` when `overall > 30` |
| `patina --diff input.txt` | show pattern-by-pattern changes |
| `patina --preview page.html` | render rewrites back onto a saved HTML page with toggles and inline diff |
| `patina --verify input.txt` | rewrite, then check MPS/fidelity floors with one retry |
@@ -158,7 +159,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 +205,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..872f293 100644
--- a/README_JA.md
+++ b/README_JA.md
@@ -6,7 +6,7 @@
[](https://opensource.org/licenses/MIT)
[](#クイックスタート)
[](https://github.com/devswha/patina)
-[](CHANGELOG.md)
+[](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 |
@@ -116,8 +116,9 @@ patina --lang [mode] [--profile ] input.txt
|---|---|
| `patina input.txt` | デフォルトで書き換え |
| `patina --audit input.txt` | パターン検出のみ |
-| `patina --score input.txt` | 0-100 の AI 類似度スコアを出力 |
-| `patina --score --exit-on 30 input.txt` | `overall > 30` で終了コード `3` を返す CI ゲート |
+| `patina --score input.txt` | LLM 判定と決定論的シグナルを併用して 0-100 のスコアを出力 |
+| `patina --score --offline input.txt` | バックエンドや API キーを使わず、決定論的シグナルのみでスコアを計算 |
+| `patina --score --offline --exit-on 30 input.txt` | `overall > 30` で終了コード `3` を返すオフライン CI ゲート |
| `patina --diff input.txt` | 変更をパターンごとに表示 |
| `patina --preview page.html` | 保存済み HTML ページ上に書き換えを反映し、トグルとインライン diff を表示 |
| `patina --verify input.txt` | 書き換え後、1 回のリトライで MPS/忠実度フロアを検査 |
@@ -140,7 +141,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 +187,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..1623dd7 100644
--- a/README_KR.md
+++ b/README_KR.md
@@ -6,7 +6,7 @@
[](https://opensource.org/licenses/MIT)
[](#빠른-시작)
[](https://github.com/devswha/patina)
-[](CHANGELOG.md)
+[](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 |
@@ -116,8 +116,9 @@ patina --lang [mode] [--profile ] input.txt
|---|---|
| `patina input.txt` | 기본값으로 재작성 |
| `patina --audit input.txt` | 패턴 탐지만 수행 |
-| `patina --score input.txt` | 0-100 AI 유사도 점수 출력 |
-| `patina --score --exit-on 30 input.txt` | `overall > 30`이면 종료 코드 `3`을 내는 CI 게이트 |
+| `patina --score input.txt` | LLM 판정과 결정론적 신호를 함께 사용해 0-100 점수 출력 |
+| `patina --score --offline input.txt` | 백엔드나 API 키 없이 결정론적 신호만으로 점수 계산 |
+| `patina --score --offline --exit-on 30 input.txt` | `overall > 30`이면 종료 코드 `3`을 내는 오프라인 CI 게이트 |
| `patina --diff input.txt` | 패턴별 변경 사항 표시 |
| `patina --preview page.html` | 저장된 HTML 페이지 위에 재작성을 다시 렌더링(토글 + 인라인 diff) |
| `patina --verify input.txt` | 재작성 후 MPS/충실도 하한을 검사하고 1회 재시도 |
@@ -140,7 +141,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 +187,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..3045338 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -6,7 +6,7 @@
[](https://opensource.org/licenses/MIT)
[](#快速开始)
[](https://github.com/devswha/patina)
-[](CHANGELOG.md)
+[](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 |
@@ -118,8 +118,9 @@ patina --lang [mode] [--profile ] input.txt
|---|---|
| `patina input.txt` | 用默认设置改写 |
| `patina --audit input.txt` | 仅检测模式 |
-| `patina --score input.txt` | 输出 0-100 的 AI 相似度分数 |
-| `patina --score --exit-on 30 input.txt` | CI gate:当 `overall > 30` 时以退出码 `3` 结束 |
+| `patina --score input.txt` | 结合 LLM 评判与确定性信号输出 0-100 分 |
+| `patina --score --offline input.txt` | 无需后端或 API key,仅使用确定性信号评分 |
+| `patina --score --offline --exit-on 30 input.txt` | 离线 CI gate:当 `overall > 30` 时以退出码 `3` 结束 |
| `patina --diff input.txt` | 按模式逐项展示改动 |
| `patina --preview page.html` | 把改写结果渲染回保存的 HTML 页面,带视图切换和内联 diff |
| `patina --verify input.txt` | 改写后检查 MPS/忠实度下限,并重试一次 |
@@ -142,7 +143,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 +189,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..b7429dc 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
@@ -20,12 +20,18 @@ allowed-tools:
## 1단계: 설정 로드
-`.patina.default.yaml`을 읽어 설정을 로드한다.
+먼저 이 `SKILL.md`와 같은 설치 디렉터리의 `.patina.default.yaml`을 기본 설정으로 읽는다. 이 파일은 배포 기본값이며 프로젝트 설정 파일이 아니다.
+
+그다음 존재하는 사용자/프로젝트 설정을 아래 순서대로 읽어 병합한다. 프로젝트 설정의 정본 파일명은 CLI와 동일한 `.patina.yaml`이다.
```
-Glob .patina.default.yaml → Read
+Read /.patina.default.yaml
+Glob ~/.patina.yaml → 있으면 Read
+Glob ./.patina.yaml → 있으면 Read
```
+우선순위는 `.patina.default.yaml` → `~/.patina.yaml` → `./.patina.yaml` → `$ARGUMENTS`다. YAML 매핑은 재귀 병합하고 `blocklist`, `allowlist`, `skip-patterns`는 중복 없이 합친다. 다른 배열은 뒤 설정의 값으로 교체한다.
+
설정에서 다음을 확인:
- `profile`: 사용할 프로필 (기본: `default`)
- `patterns`: 로드할 패턴 팩 목록
@@ -39,8 +45,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 +75,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 +137,7 @@ Read core/voice.md
> **주의:** 텍스트가 1개 단락 이하이고 2개 문장 이하이면 추출을 건너뛰고 파이프라인을 정상 실행한다 (오버헤드 불필요).
>
-> **참고:** 앵커 추출이 건너뛰어지거나 추출된 앵커가 0개이면, MPS는 N/A로 표시되며 ouroboros의 MPS 게이팅을 면제한다.
+> **참고:** 앵커 추출이 건너뛰어지거나 추출된 앵커가 0개이면, MPS는 N/A로 표시되며 검증 게이트의 MPS 하한을 적용하지 않는다.
### 앵커 유형
@@ -392,52 +395,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 +454,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 +495,6 @@ gate_result: accept | rollback
---
```
-> **참고:** `--strict`는 `--ouroboros`와 함께 사용할 수 있다. 이 경우 P5 게이트를 통과한 최종 출력이 ouroboros 루프의 입력으로 사용된다.
## 배치 모드 (`--batch`)
@@ -577,7 +533,6 @@ Glob {지정된 파일 패턴} → 파일 목록 확보
| big.md | — | — | — | ⏭️ skipped (too large) |
| broken.md | — | — | — | ❌ error: parse failed |
-> **주의:** `--batch`는 `--ouroboros`와 함께 사용할 수 있다. 이 경우 각 파일에 대해 ouroboros 루프가 독립적으로 실행된다.
---
@@ -660,7 +615,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 +639,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 +698,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 +718,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 +737,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_LANGS ⇒ Object
-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..6a26d82 100644
--- a/docs/FLAG-PARITY.md
+++ b/docs/FLAG-PARITY.md
@@ -1,6 +1,6 @@
# CLI / Skill Flag Parity
-Basis: local checkout plus `node bin/patina.js --help` and `SKILL.md` reviewed on 2026-06-04. This table separates the standalone CLI surface from the prompt-based `/patina` skill; a missing check is not always a bug when the flag is backend-, auth-, or CLI-automation-only.
+Basis: local checkout plus `node bin/patina.js --help` and `SKILL.md` reviewed on 2026-07-31. This table separates the standalone CLI surface from the prompt-based `/patina` skill; a missing check is not always a bug when the flag is backend-, auth-, or CLI-automation-only.
| Flag / command | Standalone CLI | `/patina` (`SKILL.md`) | Notes |
|---|:---:|:---:|---|
@@ -9,8 +9,9 @@ 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. |
+| `--offline` | ✓ | — | With `--score`, runs deterministic signals only and resolves no backend or credential. |
+| `--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/operations/number-safety-failure-rate-20260730.md b/docs/operations/number-safety-failure-rate-20260730.md
new file mode 100644
index 0000000..aa13868
--- /dev/null
+++ b/docs/operations/number-safety-failure-rate-20260730.md
@@ -0,0 +1,100 @@
+# Number-safety failures: measured, then found to be a probe artifact
+
+> **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 actually happens on customer-shaped input
+
+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 |
+|---|---:|---|---|---:|
+| 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 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 reasoning error
+
+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.
+
+## What remains true and worth fixing
+
+`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.
+
+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.
+
+## What this evidence does **not** justify
+
+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.
+
+## Cost, measured on the production path
+
+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:
+
+| `request.provider` | scorer thinking (MPS / fidelity) | full pipeline |
+|---|---|---:|
+| unset | 923 / 497 | $0.0573 |
+| `'gemini'` | **0 / 0** | **$0.0311** |
+
+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
+
+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.
diff --git a/docs/operations/owner-actions-go-live.md b/docs/operations/owner-actions-go-live.md
index 2b93649..8402f75 100644
--- a/docs/operations/owner-actions-go-live.md
+++ b/docs/operations/owner-actions-go-live.md
@@ -1,3 +1,12 @@
+> **TERMINAL — superseded 2026-08-03.** This runbook targets the retired Lemon
+> Squeezy route; the LS account was declined
+> ([`payment-provider-reset-20260729.md`](payment-provider-reset-20260729.md))
+> and the checkout binding table now carries only the Polar production tuple.
+> Do NOT provision the `LS_*` identities or the lemonsqueezy checkout URL
+> below. Current owner path: [`polar-binding-migration.md`](polar-binding-migration.md)
+> (env/gate sequence) + [`pro-launch.md`](pro-launch.md) (gate machinery).
+> Retained verbatim below as history.
+
# Owner actions to open Pro sales (two items, ~15 minutes)
> Companion to [`production-go-live-checklist.md`](production-go-live-checklist.md).
diff --git a/docs/operations/polar-approval-20260803.md b/docs/operations/polar-approval-20260803.md
new file mode 100644
index 0000000..eb6133b
--- /dev/null
+++ b/docs/operations/polar-approval-20260803.md
@@ -0,0 +1,45 @@
+# POLAR_APPROVAL — account approved, payout connected, identity verified (2026-08-03)
+
+> Exit evidence for the `POLAR_APPROVAL` blocker in
+> [`v6.4-preflight-hold.json`](v6.4-preflight-hold.json). Immutable record:
+> append corrections as new dated sections, never edit the observation below.
+
+## Observation
+
+Observed by the **owner** in the Polar dashboard (Finance → Account) on
+2026-08-03 and reported verbatim in the operator session:
+
+| Dashboard section | Reported state |
+|---|---|
+| Account Review | **"Account approved — Your product and organization details have been reviewed and approved."** |
+| Payout Account | **"Payout account connected — Your Stripe payout account is configured and ready to receive payouts."** |
+| Identity Verification | **"Identity verified — Your identity has been successfully verified."** |
+
+All three clear simultaneously: the up-to-14-day first-payout review described
+in [`polar-onboarding-steps.md`](polar-onboarding-steps.md) has completed, the
+Stripe Connect Express payout account is attached, and KYC is done. No external
+approval remains between the account and money reaching the owner.
+
+## Approved identities
+
+The approval covers the production identities verified by read-back in
+[`pay-b-binding-polar-20260729.json`](pay-b-binding-polar-20260729.json)
+(evidence `PAY-B-20260729-POLAR-ea8385dc-4c9c3f17`):
+
+| Identity | Value |
+|---|---|
+| Organization | `9a9180b7-2d13-422e-b9a7-316bed61c51d` |
+| Product | `ea8385dc-e21f-44bd-8ccd-2725437abb70` ("patina pro", $9.99/mo USD) |
+| Benefit | `4c9c3f17-f3b9-47cd-9ca4-4295ad3957b4` (license_keys) |
+| Production checkout URL | `https://buy.polar.sh/polar_cl_qKqtaZKLhUNJetr1h7XHY6wn8lRJEtG5DAPr02tG1pW` |
+
+## Limits of this record
+
+- Reported from the owner's authenticated dashboard session; no API field
+ exposes the same three-way review state for independent re-observation, so
+ re-verification means the owner re-opening Finance → Account.
+- Approval does not waive the remaining hold blockers: `SECRET_MANAGER`,
+ `GATE_B`, `DEP_PROD_DISABLED`, `GATE_D`, `ROLLBACK_DRILLS`, and `PAY_OPEN`
+ still gate `PATINA_PRO_CHECKOUT_ENABLED=true`.
+- Polar holds accounts to a 0.4% chargeback rate and 48-hour support response;
+ approval is ongoing compliance, not a one-time clearance.
diff --git a/docs/operations/polar-binding-migration.md b/docs/operations/polar-binding-migration.md
index 8c923d9..5942657 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
@@ -167,29 +176,58 @@ test-mode purchase** — order `8973866`, subscription `2347121`, license record
fields as Polar would be fabricating evidence for a purchase that never
happened on Polar. The staging chain has to be **re-earned**, not renamed.
-## The honest sequence
-
-1. **Polar sandbox purchase against a Vercel preview deployment.** The gate is
- already proven against the sandbox API from a local process
- ([`polar-integration-evidence-20260729.md`](polar-integration-evidence-20260729.md)),
- which is not the same as a preview deployment serving `tier=pro` to a
- browser. That run produces the replacement staging runtime evidence.
-2. **Rewrite the hold's payment identities** against Polar: provider-aware
- binding evidence, `POLAR_*` names in the secret-manager blocker,
- `POLAR_APPROVAL` in place of `LS_APPROVAL`, and the `/checkout/buy/` path
- assumption replaced with Polar's `/polar_cl_*`. Retain the Lemon Squeezy
- records as history rather than deleting them; they document why the provider
- changed.
-3. **Add the production tuple** to `scripts/checkout-evidence-bindings.mjs`
- (`https://buy.polar.sh` + `/polar_cl_qKqt…`) with the evidence ID from the
- binding artifact, then re-freeze the hashes.
-4. **Enable checkout by environment**, which is a separate deliberate act:
- `PATINA_PRO_CHECKOUT_ENABLED`, `PATINA_DEPLOYMENT_CHANNEL=production`,
- `PATINA_PRO_CHECKOUT_URL`, `PATINA_PRO_GATE_EVIDENCE_ID`.
-
-Steps 2–4 are mechanical once step 1 exists. Step 1 needs a deployment, which
-is why the button is still off while the checkout URL itself is already
-sellable.
+## The honest sequence — resolution (2026-08-03)
+
+Steps 2 and 3 are **done**; step 1 was **superseded by decision** rather than
+executed; step 4 remains the deliberate owner act.
+
+1. **Superseded.** The owner chose to rewrite the hold around the two existing
+ Polar artifacts instead of re-running a sandbox purchase against a preview
+ deployment. The rationale: the sandbox record
+ ([`polar-integration-evidence-20260729.md`](polar-integration-evidence-20260729.md))
+ already proves the purchase → license → gate mechanics with a real sandbox
+ test-card purchase, and the production zero-amount purchase
+ ([`pay-live-runtime-polar-20260729.json`](pay-live-runtime-polar-20260729.json))
+ already proves the deployed-environment half end to end — which is strictly
+ stronger than what a preview run would have shown. The supersession is
+ recorded in the hold itself as the
+ `PAY_STG_SUPERSEDED_BY_PRODUCTION_RUNTIME` decision (status `SUPERSEDED`,
+ evidence `PAY-LIVE-20260729-POLAR-ea8385dc-4c9c3f17`), not silently skipped.
+2. **Done.** `scripts/check-v6.4-preflight-hold.mjs` now validates the Polar
+ chain: `PAY-B-BINDING-POLAR-v1` + `PAY-LIVE-RUNTIME-POLAR-v1` artifacts,
+ `POLAR_APPROVAL` in place of `LS_APPROVAL`, `PATINA_LICENSE_PROVIDER` /
+ `POLAR_ORGANIZATION_ID` / `POLAR_PRO_BENEFIT_ID` in the secret-manager
+ blocker, and `https://buy.polar.sh` + `/polar_cl_*` in place of the
+ `/checkout/buy/` assumption. The Lemon Squeezy evidence files remain on
+ disk and hash-frozen as history; the state schema moved to version 4.
+3. **Done.** `scripts/checkout-evidence-bindings.mjs` contains exactly one
+ tuple — production, `PAY-B-20260729-POLAR-ea8385dc-4c9c3f17`,
+ `https://buy.polar.sh` + `/polar_cl_qKqt…` — and the hashes are re-frozen.
+ The dead Lemon Squeezy tuples were removed so a retired checkout route can
+ never be re-authorized by environment values.
+4. **Open (owner) — gated, not immediate.** Enabling checkout by environment
+ (`PATINA_PRO_CHECKOUT_ENABLED=true`,
+ `PATINA_DEPLOYMENT_CHANNEL=production`, the exact bound
+ `PATINA_PRO_CHECKOUT_URL`,
+ `PATINA_PRO_GATE_EVIDENCE_ID=PAY-B-20260729-POLAR-ea8385dc-4c9c3f17`, then
+ regenerating the launch config and redeploying) is the LAST step of the
+ [`pro-launch.md`](pro-launch.md) sequence, not a standalone act. The hold's
+ blockers still carry null evidence: `POLAR_APPROVAL`, `SECRET_MANAGER`,
+ `GATE_B`, `DEP_PROD_DISABLED` (deploy production with checkout disabled
+ first), `GATE_D`, `ROLLBACK_DRILLS`, and `PAY_OPEN` must all be satisfied
+ before the flag flips to `true`. Until then `PATINA_PRO_CHECKOUT_ENABLED`
+ stays `false`.
+
+## Known limitation: the Polar price ID is not pinned
+
+`pay-b-binding-polar-20260729.json` records the price's commercial fields
+(fixed, 999 cents, usd) read back from Polar but not the Polar price object ID,
+so the hold pins product/benefit/checkout identities as literals while the
+price is pinned only by its fields. The artifact is factsSha256-sealed, so the
+ID cannot be added retroactively without fabricating evidence; capture it in
+the next read-back artifact (e.g. the Gate-B observation) and extend
+`validatePolarBindingEvidence` then.
+
## Cosmetic mismatch worth fixing in the dashboard
diff --git a/docs/operations/pro-launch.md b/docs/operations/pro-launch.md
index 8158cb9..a6ef4da 100644
--- a/docs/operations/pro-launch.md
+++ b/docs/operations/pro-launch.md
@@ -18,16 +18,16 @@ the inputs; deploy the resulting `playground/launch-config.js` with that release
|---|---|---|
| `PATINA_PRO_CHECKOUT_ENABLED` | Missing or `false` generates disabled checkout. | Must be exactly `true`. |
| `PATINA_DEPLOYMENT_CHANNEL` | Not used while checkout is disabled. | Exactly `staging` or `production` and the binding channel. |
-| `PATINA_PRO_CHECKOUT_URL` | Not used while checkout is disabled. | Exact bound Lemon Squeezy HTTPS URL: no userinfo, port, query, fragment, alternate host/subdomain, encoded path, or trailing slash. |
+| `PATINA_PRO_CHECKOUT_URL` | Not used while checkout is disabled. | Exact bound provider HTTPS checkout URL (production: `https://buy.polar.sh/polar_cl_*`): no userinfo, port, query, fragment, alternate host/subdomain, encoded path, or trailing slash. |
| `PATINA_PRO_GATE_EVIDENCE_ID` | Not used while checkout is disabled. | Staging: `PAY-STG-`; production: `PAY-B-`; exact bound evidence ID in either case. |
`scripts/checkout-evidence-bindings.mjs` is the only authorization table. Its
-production bindings are empty and deeply frozen; environment values alone can
-never enable checkout. Add a real binding only after review of an immutable
-`PAY-STG`/`PAY-B` record and a human supplies the exact Lemon Squeezy URL. Do
-not add a placeholder or sample live binding. Invalid enabled configuration
-fails generation rather than publishing a partly enabled checkout, and explicit
-`false` wins over every other input.
+bindings are deeply frozen; environment values alone can never enable checkout.
+Add a real binding only after review of an immutable `PAY-STG`/`PAY-B` record
+and a human supplies the exact provider checkout URL (currently the Polar
+checkout link). Do not add a placeholder or sample live binding. Invalid
+enabled configuration fails generation rather than publishing a partly enabled
+checkout, and explicit `false` wins over every other input.
The generated public shape is `{schemaVersion:1, channel, enabled,
checkoutOrigin, checkoutPath, evidence}`. It contains the checkout origin and
@@ -35,7 +35,7 @@ path only, never a secret, payment-provider credential, or license key.
Keep payment-provider dashboard/API credentials and alerting/monitor webhook
credentials only in the deployment secret manager. This runbook neither assumes
-nor claims that Lemon credentials or customer receipts exist. The application
+nor claims that provider credentials or customer receipts exist. The application
accepts a customer's license only in the `Authorization: Bearer `
header; raw licenses are not a browser-config, log, or durable-memory value.
## Production monitor readiness
@@ -104,7 +104,7 @@ config prefix from `PAY-B-`.
1. Keep production checkout disabled.
2. After the immutable `PAY-STG-...` record is reviewed and a human supplies the
- exact Lemon Squeezy URL, add its exact `{channel, evidence, origin, path}`
+ exact provider checkout URL, add its exact `{channel, evidence, origin, path}`
binding to source control. Then set `PATINA_PRO_CHECKOUT_ENABLED=true`,
`PATINA_DEPLOYMENT_CHANNEL=staging`, that exact URL, and that evidence ID in
the staging deployment environment.
@@ -121,8 +121,13 @@ config prefix from `PAY-B-`.
## Gate B
Gate B authorizes a production checkout candidate, not an unreviewed payment
-integration. Require staging evidence, a reviewed immutable `PAY-B-...` record,
-the human-supplied exact production Lemon Squeezy URL and its reviewed
+integration. Require staging evidence — or, for the Polar route, the recorded
+`PAY_STG_SUPERSEDED_BY_PRODUCTION_RUNTIME` decision backed by the sandbox
+integration record and the production zero-amount runtime evidence
+(`PAY-LIVE-20260729-POLAR-ea8385dc-4c9c3f17`) — plus a reviewed immutable
+`PAY-B-...` record,
+the human-supplied exact production checkout URL (currently the Polar checkout
+link) and its reviewed
source-controlled binding, server health/admission checks, rollback owner,
support/cancel path, and telemetry and PII review.
@@ -146,7 +151,7 @@ coverage, and the 10-minute sale-close drill result. Record named approvers and
the UTC decision in the launch evidence record.
A failed or missing Gate D check means leave checkout disabled. Do not substitute
-a dashboard observation, an assumed Lemon credential, or a presumed receipt for
+a dashboard observation, an assumed provider credential, or a presumed receipt for
recorded evidence.
## Live open
@@ -154,7 +159,7 @@ recorded evidence.
1. Starting from the approved checkout-disabled production artifact, use the
reviewed source-controlled production binding and set
`PATINA_PRO_CHECKOUT_ENABLED=true`, `PATINA_DEPLOYMENT_CHANNEL=production`,
- its exact Lemon Squeezy URL, and its approved `PAY-B-...` evidence ID.
+ its exact provider checkout URL, and its approved `PAY-B-...` evidence ID.
2. Generate and deploy the browser config atomically with the approved release.
3. Inspect the deployed config and CTA. It must be production, enabled, and
limited to the bound origin/path; it must not expose secrets or a full URL
diff --git a/docs/operations/secret-manager-record-20260803.md b/docs/operations/secret-manager-record-20260803.md
new file mode 100644
index 0000000..d2cb13f
--- /dev/null
+++ b/docs/operations/secret-manager-record-20260803.md
@@ -0,0 +1,43 @@
+# SECRET_MANAGER — presence-only record, Vercel production scope (2026-08-03)
+
+> Exit evidence for the `SECRET_MANAGER` blocker in
+> [`v6.4-preflight-hold.json`](v6.4-preflight-hold.json). Names only — **no
+> values appear in this file.** Verified via `vercel env ls` / a value check
+> limited to the two boolean gate flags, from the owner's authenticated CLI.
+
+## Present in the Vercel *Production* environment
+
+`KV_REST_API_URL`, `KV_REST_API_TOKEN`, `PATINA_LICENSE_PROVIDER`,
+`POLAR_ORGANIZATION_ID`, `POLAR_PRO_BENEFIT_ID`, `PATINA_PRO_API_KEY`,
+`PATINA_LICENSE_HMAC_SECRET`, `PATINA_QUOTA_HMAC_SECRET`,
+`PATINA_FREE_API_KEY`, `PATINA_PRO_PROVIDER`, `PATINA_PRO_MODEL`,
+`PATINA_PRO_CHECKOUT_ENABLED`, `PATINA_PRO_CHECKOUT_URL`,
+`PATINA_DEPLOYMENT_CHANNEL`, `PATINA_OBSERVABILITY_REST_API_URL`,
+`PATINA_OBSERVABILITY_REST_API_TOKEN`, `CRON_SECRET`,
+`PATINA_PUBLIC_BASE_URL`, `PATINA_PUBLIC_BASE_URL_SHA256`,
+`PATINA_SYNTHETIC_OBSERVER_SECRET`, `PATINA_VERCEL_LOG_QUERY_URL`,
+`PATINA_VERCEL_LOG_QUERY_URL_SHA256`, `PATINA_VERCEL_LOG_QUERY_TOKEN`,
+`PATINA_ALERT_DISCORD_WEBHOOK`.
+
+`VERCEL_GIT_COMMIT_SHA` is injected by Vercel at build time, not a dashboard
+variable.
+
+## Gate-flag values (the only values checked)
+
+- `PATINA_PRO_ALLOW_FREE_KEY` = `false` — satisfies the "absent or exactly
+ false" requirement.
+- `PATINA_PRO_CHECKOUT_ENABLED` = `false` — checkout stays disabled.
+- `PATINA_DEPLOYMENT_CHANNEL` = `production`.
+
+## Not yet present (deliberate — set at live open)
+
+| Name | When | Value source |
+|---|---|---|
+| `PATINA_PRO_GATE_EVIDENCE_ID` | Live-open step, with the enable flag | `PAY-B-20260729-POLAR-ea8385dc-4c9c3f17` (source-controlled binding) |
+| `PATINA_SYNTHETIC_PRO_LICENSE` | Before Gate-B OBS monitoring | Owner issues a fresh license via the bounded forever-100% verification code; the prior verification license was shredded after use |
+
+## Retired names still present
+
+`LS_STORE_ID`, `LS_PRO_PRODUCT_ID`, `LS_PRO_VARIANT_ID` remain configured but
+are inert while `PATINA_LICENSE_PROVIDER=polar`. Remove them after live open
+stabilizes; they are no longer required by the rewritten hold.
diff --git a/docs/operations/v6.4-preflight-hold.json b/docs/operations/v6.4-preflight-hold.json
index 67e8768..9c08242 100644
--- a/docs/operations/v6.4-preflight-hold.json
+++ b/docs/operations/v6.4-preflight-hold.json
@@ -1,90 +1,266 @@
{
- "schemaVersion": 3,
+ "schemaVersion": 4,
"release": "v6.4",
"state": "HOLD_NO_PROMOTION",
"promotionRows": [
- { "surface": "OpenAI HTTP", "currentDefault": "gpt-5.5", "candidate": "gpt-5.6", "status": "HOLD_NO_PROMOTION" },
- { "surface": "Codex CLI", "currentDefault": "gpt-5.5", "candidate": "gpt-5.6", "status": "HOLD_NO_PROMOTION" },
- { "surface": "Claude CLI", "currentDefault": "claude-sonnet-4-6", "candidate": "claude-sonnet-5", "status": "HOLD_NO_PROMOTION" },
- { "surface": "Gemini HTTP", "currentDefault": "gemini-2.5-pro", "candidate": "gemini-3.1-pro-preview", "ceiling": "opt_in_only", "status": "HOLD_NO_PROMOTION" },
- { "surface": "Gemini CLI", "currentDefault": "gemini-2.5-pro", "candidate": "gemini-3.1-pro-preview", "ceiling": "opt_in_only", "status": "HOLD_NO_PROMOTION" }
+ {
+ "surface": "OpenAI HTTP",
+ "currentDefault": "gpt-5.5",
+ "candidate": "gpt-5.6",
+ "status": "HOLD_NO_PROMOTION"
+ },
+ {
+ "surface": "Codex CLI",
+ "currentDefault": "gpt-5.5",
+ "candidate": "gpt-5.6",
+ "status": "HOLD_NO_PROMOTION"
+ },
+ {
+ "surface": "Claude CLI",
+ "currentDefault": "claude-sonnet-4-6",
+ "candidate": "claude-sonnet-5",
+ "status": "HOLD_NO_PROMOTION"
+ },
+ {
+ "surface": "Gemini HTTP",
+ "currentDefault": "gemini-2.5-pro",
+ "candidate": "gemini-3.1-pro-preview",
+ "ceiling": "opt_in_only",
+ "status": "HOLD_NO_PROMOTION"
+ },
+ {
+ "surface": "Gemini CLI",
+ "currentDefault": "gemini-2.5-pro",
+ "candidate": "gemini-3.1-pro-preview",
+ "ceiling": "opt_in_only",
+ "status": "HOLD_NO_PROMOTION"
+ }
],
"completedDecisions": [
- { "id": "GATE_C_OPENAI_HTTP_NO_PROMOTION", "status": "HOLD_NO_PROMOTION" },
- { "id": "GATE_C_CODEX_CLI_NO_PROMOTION", "status": "HOLD_NO_PROMOTION" },
- { "id": "GATE_C_CLAUDE_CLI_NO_PROMOTION", "status": "HOLD_NO_PROMOTION" },
- { "id": "GATE_C_GEMINI_HTTP_NO_PROMOTION", "status": "HOLD_NO_PROMOTION" },
- { "id": "GATE_C_GEMINI_CLI_NO_PROMOTION", "status": "HOLD_NO_PROMOTION" },
- { "id": "PAY_STG_BINDING_APPROVAL", "status": "COMPLETED", "evidence": "PAY-STG-20260716-1199625-1875389" },
- { "id": "SOURCE_BINDING_STAGING_INTEGRATION", "status": "COMPLETED", "evidence": "PAY-STG-20260716-1199625-1875389" },
- { "id": "PAY_STG_RUNTIME_TEST_SMOKE", "status": "COMPLETED", "evidence": "PAY-STG-RUNTIME-20260716-8973866" },
- { "id": "PAY_B_BINDING_APPROVAL", "status": "COMPLETED", "evidence": "PAY-B-20260723-1236551-1932893" },
- { "id": "SOURCE_BINDING_PRODUCTION_INTEGRATION", "status": "COMPLETED", "evidence": "PAY-B-20260723-1236551-1932893" }
+ {
+ "id": "GATE_C_OPENAI_HTTP_NO_PROMOTION",
+ "status": "HOLD_NO_PROMOTION"
+ },
+ {
+ "id": "GATE_C_CODEX_CLI_NO_PROMOTION",
+ "status": "HOLD_NO_PROMOTION"
+ },
+ {
+ "id": "GATE_C_CLAUDE_CLI_NO_PROMOTION",
+ "status": "HOLD_NO_PROMOTION"
+ },
+ {
+ "id": "GATE_C_GEMINI_HTTP_NO_PROMOTION",
+ "status": "HOLD_NO_PROMOTION"
+ },
+ {
+ "id": "GATE_C_GEMINI_CLI_NO_PROMOTION",
+ "status": "HOLD_NO_PROMOTION"
+ },
+ {
+ "id": "PAY_STG_SUPERSEDED_BY_PRODUCTION_RUNTIME",
+ "status": "SUPERSEDED",
+ "evidence": "PAY-LIVE-20260729-POLAR-ea8385dc-4c9c3f17"
+ },
+ {
+ "id": "PAY_B_BINDING_APPROVAL",
+ "status": "COMPLETED",
+ "evidence": "PAY-B-20260729-POLAR-ea8385dc-4c9c3f17"
+ },
+ {
+ "id": "SOURCE_BINDING_PRODUCTION_INTEGRATION",
+ "status": "COMPLETED",
+ "evidence": "PAY-B-20260729-POLAR-ea8385dc-4c9c3f17"
+ },
+ {
+ "id": "PAY_LIVE_RUNTIME_ZERO_AMOUNT_SMOKE",
+ "status": "COMPLETED",
+ "evidence": "PAY-LIVE-20260729-POLAR-ea8385dc-4c9c3f17"
+ }
],
- "checkout": { "enabled": false },
+ "checkout": {
+ "enabled": false
+ },
"frozenSemantics": {
- "manifestVersion": 3,
+ "manifestVersion": 4,
"providers": {
- "openai": { "name": "openai", "baseURL": "https://api.openai.com/v1", "apiKeyEnv": "OPENAI_API_KEY", "defaultModel": "gpt-5.5", "freeTier": false, "note": "Paid. Default OpenAI Platform API." },
- "gemini": { "name": "gemini", "baseURL": "https://generativelanguage.googleapis.com/v1beta/openai", "apiKeyEnv": "GEMINI_API_KEY", "defaultModel": "gemini-2.5-pro", "freeTier": true, "note": "Free tier available. Get a key at https://aistudio.google.com/app/apikey" },
- "groq": { "name": "groq", "baseURL": "https://api.groq.com/openai/v1", "apiKeyEnv": "GROQ_API_KEY", "defaultModel": "llama-3.3-70b-versatile", "freeTier": true, "note": "Free tier with rate limits. Get a key at https://console.groq.com/keys" },
- "kimi": { "name": "kimi", "baseURL": "https://api.moonshot.ai/v1", "apiKeyEnv": "KIMI_API_KEY", "defaultModel": "kimi-k2.5", "freeTier": false, "note": "Moonshot AI Kimi OpenAI-compatible API. Set KIMI_API_KEY or PATINA_API_KEY." },
- "moonshot": { "name": "moonshot", "baseURL": "https://api.moonshot.ai/v1", "apiKeyEnv": "MOONSHOT_API_KEY", "defaultModel": "kimi-k2.5", "freeTier": false, "note": "Moonshot AI Kimi OpenAI-compatible API. Set MOONSHOT_API_KEY or PATINA_API_KEY." },
- "together": { "name": "together", "baseURL": "https://api.together.xyz/v1", "apiKeyEnv": "TOGETHER_API_KEY", "defaultModel": "meta-llama/Llama-3.3-70B-Instruct-Turbo-Free", "freeTier": true, "note": "Free models available (suffix \"-Free\"). Get a key at https://api.together.xyz/settings/api-keys" }
+ "openai": {
+ "name": "openai",
+ "baseURL": "https://api.openai.com/v1",
+ "apiKeyEnv": "OPENAI_API_KEY",
+ "defaultModel": "gpt-5.5",
+ "freeTier": false,
+ "note": "Paid. Default OpenAI Platform API."
+ },
+ "gemini": {
+ "name": "gemini",
+ "baseURL": "https://generativelanguage.googleapis.com/v1beta/openai",
+ "apiKeyEnv": "GEMINI_API_KEY",
+ "defaultModel": "gemini-2.5-pro",
+ "freeTier": true,
+ "note": "Free tier available. Get a key at https://aistudio.google.com/app/apikey"
+ },
+ "groq": {
+ "name": "groq",
+ "baseURL": "https://api.groq.com/openai/v1",
+ "apiKeyEnv": "GROQ_API_KEY",
+ "defaultModel": "llama-3.3-70b-versatile",
+ "freeTier": true,
+ "note": "Free tier with rate limits. Get a key at https://console.groq.com/keys"
+ },
+ "kimi": {
+ "name": "kimi",
+ "baseURL": "https://api.moonshot.ai/v1",
+ "apiKeyEnv": "KIMI_API_KEY",
+ "defaultModel": "kimi-k2.5",
+ "freeTier": false,
+ "note": "Moonshot AI Kimi OpenAI-compatible API. Set KIMI_API_KEY or PATINA_API_KEY."
+ },
+ "moonshot": {
+ "name": "moonshot",
+ "baseURL": "https://api.moonshot.ai/v1",
+ "apiKeyEnv": "MOONSHOT_API_KEY",
+ "defaultModel": "kimi-k2.5",
+ "freeTier": false,
+ "note": "Moonshot AI Kimi OpenAI-compatible API. Set MOONSHOT_API_KEY or PATINA_API_KEY."
+ },
+ "together": {
+ "name": "together",
+ "baseURL": "https://api.together.xyz/v1",
+ "apiKeyEnv": "TOGETHER_API_KEY",
+ "defaultModel": "meta-llama/Llama-3.3-70B-Instruct-Turbo-Free",
+ "freeTier": true,
+ "note": "Free models available (suffix \"-Free\"). Get a key at https://api.together.xyz/settings/api-keys"
+ }
},
"sourceHashes": {
- ".env.example": "ed39e3edd12739e3c901824a786b28a7dc172e9710307425def88eee9c29bc30",
+ ".env.example": "e7d1762137e29232217712bdc7c699f6abd24595271f31dc2528fcb92c2cb00d",
"src/model-defaults.js": "c568977fcac8ea44d5387a8a8745b062675ec94d73d39ce528c492ad35f87176",
"src/providers.js": "92415eacaf87da2d0f2aed7db97feeb98b8b087adfe584a02c4478353b807d90",
- "src/web-rewrite-contract.js": "b8a9d0fc28921740b68faf464b7848721ddf6b40696139447c16560811c036f7",
+ "src/web-rewrite-contract.js": "be433e8260e452028eb53b5c1aed0935a4150cd669ec2d5e826302219df22b9a",
"playground/launch-config.js": "4d19fc8ce36651f73f94d80bbcd108a2ccbb50fa3fc25b54d75f193b42ac6bf4",
"vercel.json": "37a54c0850db54e80eec963c570d4b8a047fcfb4bf56d1a133e5a3934b28260e",
- "scripts/checkout-evidence-bindings.mjs": "8dcdf4b23787f91c773b6d8b30857ae3bc90b9f6e1552561749b00c8502e1408",
+ "scripts/checkout-evidence-bindings.mjs": "767c832fe4b517824e06fa7e18d6dd777e6525831dcfaa2022e51a4c8f27c07c",
"scripts/generate-launch-config.mjs": "c5047625479bc687c510b6faf5045cf6118359ba978d219cea834325de5e8a07",
"scripts/check-v6.4-release-ready.mjs": "fc4521db8f4677e03ac6a2199a86917b6332033030a9e8fe248cd166b0a59313",
"docs/AUTHENTICATION.md": "e8c335550c4a0f0b144b79f4286d467a968962821a1c88049f1e679810751cfe",
"docs/AUTHENTICATION_KR.md": "5e077cd3a13c2299a11fc831c8303a204880c91dd0d465148d34a435dbe00796",
- "docs/operations/pro-launch.md": "5a3ecb35a60374ee34111ad91e646a02f974439e9a2052ceb18b3ebc729d5acc",
+ "docs/operations/pro-launch.md": "50448491ce0b745eb2bfe07983238a6e727466de0f71c72b7cbc4a9c447068ff",
"docs/operations/pay-stg-binding-20260716.json": "2f523259de91f640f056fe7acfe00264e493d9891b7a61152fe5e91704c0ecdf",
"docs/operations/pay-stg-runtime-20260716.json": "b0229e892b06e1ec303a001c1317c4c63fc3c98fd7e10243e64db07df4803d29",
"docs/operations/pay-b-binding-20260723.json": "96eb8e0aba9fcb4ce67dd356bd35aaf678d8abea96a7ec134218eab7cd20f132",
+ "docs/operations/pay-b-binding-polar-20260729.json": "471c9035a04ce5c2c76d7e5b4d595054191d513fa007455a02c9b123d0811ac5",
+ "docs/operations/pay-live-runtime-polar-20260729.json": "384912dca4576f1c3f8d653f39c3d9a8cb19f8f8acc1a91ac2bc36fdf10e7a9c",
"tests/e2e/providers.test.js": "47958be678e9bbfd06bcdd849ec0df37ed765f886e245b7b528e7d596b6d9dfe",
"tests/unit/backend-model-defaults.test.js": "908dd9b06a9d5305ad28b50007d6428435b0b6b7019d6a2210aff813bfb9cf3d",
- "tests/unit/web-deploy-invariants.test.js": "0c858c964a350115af6b567d8f3f707cc6749ede5e2967ab56f30b2fa0fb8bcd",
+ "tests/unit/web-deploy-invariants.test.js": "2aecb9068c04999eaf964766cdc1e0c3a551f58ecfa54a0ade8802f1b8f4f296",
"tests/unit/web-rewrite-contract.test.js": "2980f555bb53e7c8d93a74d60dc096adadfd1c7d07fd70f55de1670959219c13",
"tests/unit/web-rewrite-contract.redteam.test.js": "3c7591926e168b14f486199297fd1e84ed447c1b8d339c9b4860f79da0f6a7bb",
- "tests/unit/v6.4-preflight-hold.test.js": "a14660535c998cde52e8810fec7812305d1688af67927cc868524040c2eafd17",
+ "tests/unit/v6.4-preflight-hold.test.js": "c5f8a3465576dff5a94f07dd8eaac741d1e0e5006b7076fe9884204eb7a0c7a8",
"tests/unit/v6.4-release-ready.test.js": "8d88aab35ce75bc40b4782932329c0402001afabc380ac349ccc1b82d01c12d7",
- "package.json": "1e8ac85400d4d5650f9ad2e4bf64af2e5706b33543b4654ccf96b52da5505503",
- "package-lock.json": "d2daab24b2f2659b18cbb31f82c4725d946b1c7b3a0c677de61b5b1ab4c73d2c",
+ "package.json": "6712053362194ab947aa10de4f7772bfa23ae6db97dc5a912e05763c4e3a2dc3",
+ "package-lock.json": "3d4b2573cd3a273d766a01a6ea2245b2528a9149ae75fdda9bb977bfd5af69a1",
".github/workflows/release.yml": "43900a0966a52c500d54b1971d4141fe2d380a893364b4cb7b9976e9e3795f8f",
- "README.md": "9432759281d3f515dabc60565b91452526638d0100eec1592ddf9eb8b88bb1a7",
- "README_KR.md": "24b57094c56177111cac916cdb9dbd66cf58f8f9a0c148af7b683968b7d59be6",
- "README_ZH.md": "a03339478ee079af4f4ad677eb6b0f4eb0c53225496bf5dc1198953f38a36a38",
- "README_JA.md": "c76900d114230e3d2591637b1a2869a157ba467d5df7086cc3c3bd05c0755165",
- "SKILL.md": "61c4375aaee481ec57c713bda7e5a58df0778ef58bfdb0513ccee8d1bf583b67",
- ".patina.default.yaml": "f9c43c8e32a8b62f266a379a8ea7a99b74967b49821e84602a5e71f02c7dfc31",
- "packages/patina-humanizer/package.json": "aacfc932d6839c5ce99e70c96160159afb81e6e0d3802a77387d85e7919c516b",
- ".claude-plugin/plugin.json": "9a439cad38598dbe82f4fd45534bc225776cf05613700c7bde2cb7831f20c18e",
- ".claude-plugin/marketplace.json": "2e8fee2466bb0d5ffcb3bd8a4731e70028e587425c2ee97edbeb3f926f54cace",
- "CHANGELOG.md": "9996dc886df3ab5267cd364c7d3e7782cb7991851a7262574f70037dd7f070b2"
+ "README.md": "78829adef9bc233a2dc0003ea022281ce483cb958daeef01a073361e31ce8f11",
+ "README_KR.md": "ebac4f175b399a081d4c4a2b92fcc0708f1e85976d8da869f0f9b608046cfd59",
+ "README_ZH.md": "66076f5870bf806310b4516db0c260037f7b2684c3234b291660593f744a8ec0",
+ "README_JA.md": "bd71aa30bd9eca7b1f4520f4fb609585f40ccbf78d9c0ba29d29c7364c869c44",
+ "SKILL.md": "3b7dc62f22c1feb9178d49a47e78bfa60fe94ea314d7132a99dcc6c9b231a4c5",
+ ".patina.default.yaml": "308311b43add43e9d0e40bdfcdf65823337f12357e50366ce0fb8b07cfd76885",
+ "packages/patina-humanizer/package.json": "fc9b27a4c97965d17b18492999f7018f34819806d63d626eeb428f6dd0834c63",
+ ".claude-plugin/plugin.json": "7a5df3e87832d22821e7d1fafed76e125b33a02d5550e46fa73e7aa9073d2325",
+ ".claude-plugin/marketplace.json": "c500dfec67c40f4c2acb6de3efc5c60631e8e82ff961311a09609fe37e806e2d",
+ "CHANGELOG.md": "dfc4fb1f70ca873ca1bf44b3949374d43018fc673b98212e7515b31c9a07ef80"
}
},
"blockers": [
- { "id": "LS_APPROVAL", "type": "human_action", "owner": "Lemon Squeezy Approval Owner", "exitEvidence": "Immutable Lemon Squeezy approval naming the exact store, product, and variant identities; exact staging and production HTTPS checkout URLs; and approval that is immutable.", "evidence": null },
- { "id": "SECRET_MANAGER", "type": "human_action", "owner": "Secret Manager Owner", "exitEvidence": "Secret-manager presence-only record, without values, for KV_REST_API_URL, KV_REST_API_TOKEN, LS_STORE_ID, LS_PRO_PRODUCT_ID, LS_PRO_VARIANT_ID, PATINA_PRO_API_KEY, PATINA_LICENSE_HMAC_SECRET, PATINA_QUOTA_HMAC_SECRET, PATINA_FREE_API_KEY, PATINA_PRO_PROVIDER, PATINA_PRO_MODEL, PATINA_PRO_CHECKOUT_ENABLED, PATINA_PRO_CHECKOUT_URL, PATINA_PRO_GATE_EVIDENCE_ID, PATINA_DEPLOYMENT_CHANNEL, PATINA_OBSERVABILITY_REST_API_URL, PATINA_OBSERVABILITY_REST_API_TOKEN, CRON_SECRET, PATINA_PUBLIC_BASE_URL, PATINA_PUBLIC_BASE_URL_SHA256, PATINA_SYNTHETIC_PRO_LICENSE, PATINA_SYNTHETIC_OBSERVER_SECRET, PATINA_VERCEL_LOG_QUERY_URL, PATINA_VERCEL_LOG_QUERY_URL_SHA256, PATINA_VERCEL_LOG_QUERY_TOKEN, PATINA_ALERT_DISCORD_WEBHOOK, VERCEL_GIT_COMMIT_SHA; PATINA_PRO_ALLOW_FREE_KEY is absent or exactly false.", "evidence": null },
- { "id": "PAY_STG", "type": "human_action", "owner": "Deployment Owner", "exitEvidence": "Immutable private-staging PAY-STG deployment record naming the exact staging HTTPS checkout URL and completed staging source-binding integration commit or artifact.", "evidence": null },
- { "id": "GATE_B", "type": "human_action", "owner": "Payment Runtime Owner + maintainer", "exitEvidence": "Gate-B approval by the Payment Runtime Owner and maintainer proving completed production source-binding integration commit or artifact, hosted identity, usage, dedicated runtime, content-valid PAY-B-COST evidence, and real-path OBS evidence.", "evidence": null },
- { "id": "DEP_PROD_DISABLED", "type": "human_action", "owner": "Deployment Owner", "exitEvidence": "Immutable production-disabled artifact/config with the exact disabled launch shape; browser evidence that checkout is disabled; health, monitor, and operator evidence; and UTC timestamps.", "evidence": null },
- { "id": "GATE_D", "type": "human_action", "owner": "Release Authority", "exitEvidence": "Gate-D record proving mandatory gates passed and naming the approving Release Authority.", "evidence": null },
- { "id": "ROLLBACK_DRILLS", "type": "human_action", "owner": "Deployment Owner", "exitEvidence": "Timestamped rollback records: sale-close completed within 10 minutes, plus correctness evidence for service-kill and fallback without a 10-minute claim.", "evidence": null },
- { "id": "PAY_OPEN", "type": "human_action", "owner": "Maintainer", "exitEvidence": "Maintainer authorization to open payment after the required gates and immutable evidence are complete.", "evidence": null },
- { "id": "PAY_LIVE", "type": "human_action", "owner": "Payment Runtime Owner", "exitEvidence": "Bounded real-production payment, refund, revoke, license-denial, and recovery evidence, including denial and recovery within the documented propagation bound.", "evidence": null },
- { "id": "REL_PUBLISH", "type": "human_action", "owner": "Release Authority", "exitEvidence": "Final Release Authority approval for the v6.4 tag and registry publication.", "evidence": null }
+ {
+ "id": "POLAR_APPROVAL",
+ "type": "human_action",
+ "owner": "Polar Approval Owner",
+ "exitEvidence": "Immutable Polar account approval at first payout naming the exact production organization, product, and benefit identities and the exact production HTTPS checkout URL; and approval that is immutable.",
+ "evidence": null
+ },
+ {
+ "id": "SECRET_MANAGER",
+ "type": "human_action",
+ "owner": "Secret Manager Owner",
+ "exitEvidence": "Secret-manager presence-only record, without values, for KV_REST_API_URL, KV_REST_API_TOKEN, PATINA_LICENSE_PROVIDER, POLAR_ORGANIZATION_ID, POLAR_PRO_BENEFIT_ID, PATINA_PRO_API_KEY, PATINA_LICENSE_HMAC_SECRET, PATINA_QUOTA_HMAC_SECRET, PATINA_FREE_API_KEY, PATINA_PRO_PROVIDER, PATINA_PRO_MODEL, PATINA_PRO_CHECKOUT_ENABLED, PATINA_PRO_CHECKOUT_URL, PATINA_PRO_GATE_EVIDENCE_ID, PATINA_DEPLOYMENT_CHANNEL, PATINA_OBSERVABILITY_REST_API_URL, PATINA_OBSERVABILITY_REST_API_TOKEN, CRON_SECRET, PATINA_PUBLIC_BASE_URL, PATINA_PUBLIC_BASE_URL_SHA256, PATINA_SYNTHETIC_PRO_LICENSE, PATINA_SYNTHETIC_OBSERVER_SECRET, PATINA_VERCEL_LOG_QUERY_URL, PATINA_VERCEL_LOG_QUERY_URL_SHA256, PATINA_VERCEL_LOG_QUERY_TOKEN, PATINA_ALERT_DISCORD_WEBHOOK, VERCEL_GIT_COMMIT_SHA; PATINA_PRO_ALLOW_FREE_KEY is absent or exactly false.",
+ "evidence": null
+ },
+ {
+ "id": "GATE_B",
+ "type": "human_action",
+ "owner": "Payment Runtime Owner + maintainer",
+ "exitEvidence": "Gate-B approval by the Payment Runtime Owner and maintainer proving completed production source-binding integration commit or artifact, hosted identity, usage, dedicated runtime, content-valid PAY-B-COST evidence, and real-path OBS evidence.",
+ "evidence": null
+ },
+ {
+ "id": "DEP_PROD_DISABLED",
+ "type": "human_action",
+ "owner": "Deployment Owner",
+ "exitEvidence": "Immutable production-disabled artifact/config with the exact disabled launch shape; browser evidence that checkout is disabled; health, monitor, and operator evidence; and UTC timestamps.",
+ "evidence": null
+ },
+ {
+ "id": "GATE_D",
+ "type": "human_action",
+ "owner": "Release Authority",
+ "exitEvidence": "Gate-D record proving mandatory gates passed and naming the approving Release Authority.",
+ "evidence": null
+ },
+ {
+ "id": "ROLLBACK_DRILLS",
+ "type": "human_action",
+ "owner": "Deployment Owner",
+ "exitEvidence": "Timestamped rollback records: sale-close completed within 10 minutes, plus correctness evidence for service-kill and fallback without a 10-minute claim.",
+ "evidence": null
+ },
+ {
+ "id": "PAY_OPEN",
+ "type": "human_action",
+ "owner": "Maintainer",
+ "exitEvidence": "Maintainer authorization to open payment after the required gates and immutable evidence are complete.",
+ "evidence": null
+ },
+ {
+ "id": "PAY_LIVE",
+ "type": "human_action",
+ "owner": "Payment Runtime Owner",
+ "exitEvidence": "Bounded real-production payment, refund, revoke, license-denial, and recovery evidence, including denial and recovery within the documented propagation bound.",
+ "evidence": null
+ },
+ {
+ "id": "REL_PUBLISH",
+ "type": "human_action",
+ "owner": "Release Authority",
+ "exitEvidence": "Final Release Authority approval for the v6.4 tag and registry publication.",
+ "evidence": null
+ }
],
"deferredActions": [
- { "id": "V6_4_METADATA_COPY_RECONCILIATION", "type": "repo_action", "blockedBy": ["GATE_B"], "reason": "Cannot reconcile 6.4 metadata and copy until Gate B evidence exists; Gate-C no-promotion cutoff decisions are complete.", "executable": false },
- { "id": "FINAL_TAG_PUBLISH_COMMAND", "type": "repo_action", "blockedBy": ["PAY_LIVE", "REL_PUBLISH"], "reason": "Cannot run the final tag and publish command until named external evidence exists.", "executable": false }
+ {
+ "id": "V6_4_METADATA_COPY_RECONCILIATION",
+ "type": "repo_action",
+ "blockedBy": [
+ "GATE_B"
+ ],
+ "reason": "Cannot reconcile 6.4 metadata and copy until Gate B evidence exists; Gate-C no-promotion cutoff decisions are complete.",
+ "executable": false
+ },
+ {
+ "id": "FINAL_TAG_PUBLISH_COMMAND",
+ "type": "repo_action",
+ "blockedBy": [
+ "PAY_LIVE",
+ "REL_PUBLISH"
+ ],
+ "reason": "Cannot run the final tag and publish command until named external evidence exists.",
+ "executable": false
+ }
],
- "prohibitions": { "tag": true, "publish": true }
+ "prohibitions": {
+ "tag": true,
+ "publish": true
+ }
}
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