Land pack-17 delta: third content-policy action allow asserts capability without routing, making the ratified label pair expressible in data (D39.28)

This commit is contained in:
Claude (backend session) 2026-07-25 17:55:51 +03:00
parent 4e430266f1
commit 340de85916
13 changed files with 416 additions and 45 deletions

View file

@ -14,7 +14,7 @@ Go-бэкенд издательского художественного пер
## Источники истины (по убыванию)
1. **`docs/architecture/05-decisions-log.md` (D1D39.27)** — ратифицированный контракт; при конфликте с любым другим доком побеждает он; сверху файла — карта актуальности (что чем superseded). **Правило чтения (онбординг-диета 25.07): карта + живая голова (эра D39.x); корпус D1D38 = справочник — по ссылкам/grep по D-номеру, целиком НЕ читать.**
1. **`docs/architecture/05-decisions-log.md` (D1D39.28)** — ратифицированный контракт; при конфликте с любым другим доком побеждает он; сверху файла — карта актуальности (что чем superseded). **Правило чтения (онбординг-диета 25.07): карта + живая голова (эра D39.x); корпус D1D38 = справочник — по ссылкам/grep по D-номеру, целиком НЕ читать.**
2. `docs/experiments/00-provider-quirks.md` — wire-квирки провайдеров (читать ПЕРЕД правкой адаптеров/вызовами провайдеров).
3. `docs/architecture/09-target-architecture.md` (7 слоёв; статус стройки — шапка-таблица) · `12-go-style-notes.md` (норматив общности §0) · `10-prompt-architecture.md` (промпт-тема).
4. `docs/architecture/01-decisions.md` (Р1Р10), `02-mvp-plan.md`, `04-unhappy-paths.md`, `06-memory-risk-registry.md` — тела исторические, читать через ⚠-баннеры 25.07.
@ -39,11 +39,11 @@ Go-бэкенд издательского художественного пер
2. `docs/architecture/05-decisions-log.md`: карта актуальности (шапка) + живая голова (D39.x). Целиком НЕ читать — корпус D1D38 по ссылкам/grep по мере надобности.
3. По роли: Бэкенд — `backend/README.md` + `03-implementation-notes.md` (через баннер) + `12-go-style-notes.md`; Полигон — `eval/README.md` + `experiments/00` + `09-pilot-protocol.md`; всем — шапка-таблица `09-target-architecture.md` (статус слоёв).
## Текущее состояние (2026-07-25, пост-пак-17 — механизм content-labels построен и залендён, D39.27 — эра «механизация выпускного качества»)
## Текущее состояние (2026-07-25, пак-17 ЗАКРЫТ ЦЕЛИКОМ — механизм content-labels живой, D39.26D39.28 — эра «механизация выпускного качества»)
- **Фазы:** Ф0 ✅ · Ф1-инфра ✅ (D20D28; golden = инвариант детерминизма №8) · арка «качество-первым» D26→D38.5 закрыта · **арх-ресет D39 исполнен ЦЕЛИКОМ**: 7 слоёв построены паками 1116 (D39.13D39.24; статус слоёв — шапка-таблица `09-target-architecture.md`; слой-3 diff-редактор — за `enabled:false`, включение = решение владельца по замеру остатка).
- **Пере-прогон rerun2 прочитан (D39.20):** операционка живьём ($0-резюм ×3 арма, $6.63<$15, echo 0, майнинг-стоп→подпись); планка ≤2 НЕ пройдена; анти-корреляция гладкость↔верность ×3 → mistral-редактор вон. **Эксп ЗАКРЫТ (D39.22), итерация №2 отложена; редактор = deepseek-v4-pro ИНТЕРИМ** (glm-5 резерв). Стайл-каноны D39.21 (时辰=2ч · generic-«гу» ср. род · стихи смыслом · 资质=«талант»).
- **Курс — разработка бэкенда:** **пак-17 «канал B вживую» ЗАЛЕНДЕН ЦЕЛИКОМ (D39.26 дизайн + D39.27 стройка); открыт хвост-дельта «assert-only action» под ратифицированный ПАРНЫЙ вокабуляр `violence`/`sexually-explicit`** (**D39.25: в движке НЕТ понятия «18+»** — generic content-labels × provider-capabilities, refusal причино-агностичен; лейблы вне хешей · резолв до валидации · один хоп `chain[0]` · гранулярность = книга; на владельце — вокабуляр лейблов и редактор под лейблом) → следующий платный прогон (разом: итерация №2 dspro-vs-glm · замер остатка ремонта · живой канал B) → сид-дельта (元 · 赤城 · 学堂家老 · 春秋蝉) → Ф2-механизмы (голос/состояние D21 · native-Gemini судья) · ja→ru §B5 → пилот Ф2.5.
- **Курс — разработка бэкенда:** **пак-17 «канал B вживую» ЗАКРЫТ ЦЕЛИКОМ (D39.26 дизайн + D39.27 стройка + D39.28 дельта `allow`); вокабуляр = ПАРА `violence`/`sexually-explicit`, `accepts_labels` ждут ФАКТОВ ToS по первоисточникам (fail-closed — верное состояние)** (**D39.25: в движке НЕТ понятия «18+»** — generic content-labels × provider-capabilities, refusal причино-агностичен; лейблы вне хешей · резолв до валидации · один хоп `chain[0]` · гранулярность = книга; на владельце — вокабуляр лейблов и редактор под лейблом) → следующий платный прогон (разом: итерация №2 dspro-vs-glm · замер остатка ремонта · живой канал B) → сид-дельта (元 · 赤城 · 学堂家老 · 春秋蝉) → Ф2-механизмы (голос/состояние D21 · native-Gemini судья) · ja→ru §B5 → пилот Ф2.5.
- **Стек:** draft deepseek-v4-flash thinking-ON → editor deepseek-v4-pro → судья gemini-preview + grok-фолбэк 18+ (~$0.85/ранобэ D30.4). Ключи: DEEPSEEK, ZAI, KIMI, OPENAI, GEMINI, XAI, MISTRAL (xAI: единый, data-sharing, off перед продом D27).
- **Висит на владельце + открытые петли:** трекер в CURRENT-STATE `docs/PROGRESS.md` (единственное место).
- **Стенд:** WSL2, GTX 1070 8GB (localhost из-под прокси = 403 — no-proxy транспорт для local). Книга `/home/ubuntu/books/gu-zhenren/` (GB18030; текст и производные ВНЕ git).

View file

@ -72,13 +72,16 @@ providers:
# СПОСОБНОСТЬ ПО КОНТЕНТУ (D39.25/D39.26). Флаг `permissive: true` ОТСТАВЛЕН (падает громко):
# вместо булева «можно 18+» endpoint объявляет, КАКИЕ лейблы ему разрешено ПОЛУЧАТЬ, и книга
# маршрутизируется только в модели, чей набор её лейблы покрывает. Значения — ДАННЫЕ; движок не
# знает ни одного. Пока строка ПУСТА СОЗНАТЕЛЬНО (fail-closed): вокабуляр лейблов — открытое
# решение владельца (D39.26 добор A), потому что единый «adult» схлопывает ратифицированную
# развязку D14 п.1 (ToS DeepSeek запрещает sexually explicit, но violence оставляет допустимым —
# то есть violence-тяжёлая книга под единым лейблом теряет dspro-редактора без нужды).
# Форма ответа = одна строка данных, например:
# accepts_labels: [adult] # единый лейбл
# accepts_labels: [violence, sexually-explicit] # раздельный вокабуляр (сохраняет D14 п.1)
# знает ни одного. ВОКАБУЛЯР РЕШЁН владельцем (D39.27): ПАРА `violence` / `sexually-explicit` —
# она сохраняет ратифицированную развязку D14 п.1 (ToS DeepSeek запрещает sexually explicit, но
# violence оставляет допустимым), то есть violence-книга не теряет dspro-редактора.
# Строка ПУСТА СОЗНАТЕЛЬНО и это ВЕРНОЕ состояние (fail-closed): значения — ФАКТЫ ToS/AUP, а они
# (а) добываются только по ПЕРВОИСТОЧНИКАМ (правило двух направлений, `experiments/00-provider-quirks.md`),
# (б) даются ПЕР-ЛЕЙБЛУ, а не пер-провайдеру целиком: violence и sexually-explicit — РАЗНЫЕ ответы у
# одного и того же вендора, и (в) фиксируются полигоном, не бэкенд-сессией. По памяти не заполнять.
# Форма, когда факты будут (одна строка на провайдера, только подтверждённые лейблы):
# accepts_labels: [violence] # эндпойнт вправе получать только violence
# accepts_labels: [violence, sexually-explicit] # подтверждены оба
# Эмпирика этого endpoint: главный пермиссив-тир канала B + reasoning-ON эскалация (D3/D19.1),
# первичный судья эротики (D22.6).
timeouts: { attempt_s: 240, max_attempts: 3, backoff_cap_s: 60 }

View file

@ -120,16 +120,22 @@ escalation:
# движок не знает ни одного значения, а вокабуляр 18+ ждёт решения владельца, D39.26 добор A):
#
# content_policy:
# - { label: <лейбл-книги>, action: route, chain: <имя-цепочки> } # маршрут: label_models + один хоп
# - { label: <лейбл-1>, action: allow } # только АССЕРТ: маршрут не меняется
# - { label: <лейбл-2>, action: route, chain: <имя-цепочки> } # маршрут: label_models + один хоп
# # action: terminal — книга вообще не обрабатывается (отказ ВХОДА по объявленным данным; это НЕ скрин
# # контента: ратифицированный пер-чанковый L3-скрин D22.7 остаётся отдельным предусловием прода).
# stages:
# - name: draft
# label_models: { <лейбл-книги>: <модель-с-accepts_labels> }
# label_models: { <лейбл-2>: <модель-с-accepts_labels> } # только для route-лейбла
#
# Требования загрузки для route-лейбла: цепочка существует и содержит РОВНО один член ·
# escalation.budget_usd > 0 · каждая достижимая модель несёт все лейблы книги (иначе загрузка translate
# падает, называя модель, провайдера и недостающий лейбл; read-only проекции остаются доступны).
# ВСЕ ТРИ действия несут ОДИН инвариант: каждая достижимая модель обязана принимать все лейблы книги
# (иначе загрузка translate падает, называя модель, провайдера и недостающий лейбл; read-only проекции
# остаются доступны). Различаются они остальным:
# • allow — эндпойнты уже верные, ничего не двигаем; chain/label_models/бюджет НЕ нужны и ЗАПРЕЩЕНЫ;
# • route — цепочка существует и содержит РОВНО один член · escalation.budget_usd > 0 ·
# хотя бы одна стадия объявляет label_models для этого лейбла;
# • terminal — ни одного вызова.
# Пара «один лейбл ассертит, второй пере-маршрутизирует» выражается ДАННЫМИ — Go не трогается.
fanout:
candidates: 1 # C1: один черновик

View file

@ -188,10 +188,15 @@ func TestContentRoutingRefusals(t *testing.T) {
want: "no hop can ever fire",
},
{
// A terminal policy takes no per-stage routes either (they would be meaningless), so the fixture
// drops them together with the chain — that pairing is itself checked by the shape test below.
name: "terminal policy refuses the book",
pipeline: strings.Replace(basePipeline,
pipeline: strings.NewReplacer(
" - { label: restricted-synth, action: route, chain: restricted }",
" - { label: restricted-synth, action: terminal }", 1),
" - { label: restricted-synth, action: terminal }",
" label_models: { restricted-synth: capable-model }\n", "",
" label_models: { restricted-synth: second-model }\n", "",
).Replace(basePipeline),
labels: []string{"restricted-synth"},
want: "action=terminal",
},
@ -252,7 +257,7 @@ func TestContentPolicyShapeIsFatal(t *testing.T) {
want string
}{
{"multi-member chain", strings.Replace(basePipeline, "restricted: [second-model]", "restricted: [second-model, capable-hop]", 1), "MULTI-HOP IS NOT BUILT"},
{"unknown action", strings.Replace(basePipeline, "action: route, chain: restricted", "action: quarantine, chain: restricted", 1), "action must be route|terminal"},
{"unknown action", strings.Replace(basePipeline, "action: route, chain: restricted", "action: quarantine, chain: restricted", 1), "action must be route|allow|terminal"},
{"route without chain", strings.Replace(basePipeline, "action: route, chain: restricted", "action: route", 1), "requires `chain:`"},
{"chain not defined", strings.Replace(basePipeline, "action: route, chain: restricted", "action: route, chain: nosuch", 1), "not defined in escalation.chains"},
{"duplicate registry label", strings.Replace(basePipeline,
@ -687,3 +692,166 @@ models:
})
}
}
// TestAllowActionAssertsWithoutRouting covers the third policy shape: `allow` applies the capability
// invariant and changes NO route. It is the shape a label needs when the providers are already correct —
// with only route/terminal such a label was inexpressible (route demands a chain, a budget and a
// per-stage model it has no use for; terminal refuses the book), which is why a two-label vocabulary
// where one label re-routes and the other only asserts could not be written as DATA at all.
func TestAllowActionAssertsWithoutRouting(t *testing.T) {
// An allow-only registry: no chain, no budget, no label_models anywhere.
allowOnly := `core: C1
version: 1
defaults: { max_output_ratio: 2, min_max_tokens: 512 }
content_policy:
- { label: restricted-synth, action: allow }
stages:
- name: draft
role: translator
model: capable-model
prompt_override: @PROMPT@
prompt_version: v
escalate_to: capable-hop
- name: edit
role: editor
model: second-model
prompt_override: @PROMPT@
prompt_version: v
`
_, load := labelFixture(t, allowOnly)
p, err := load([]string{"restricted-synth"})
if err != nil {
t.Fatalf("an allow-only policy must need no chain and no budget: %v", err)
}
if err := p.ContentRoutingError(); err != nil {
t.Fatalf("every reachable model accepts the label, so this must be runnable: %v", err)
}
// The route is untouched — that is the whole semantic.
for _, st := range p.Stages {
if st.ResolvedModel != st.Model || st.ResolvedHop != st.EscalateTo {
t.Errorf("stage %q must keep its configured route under action=allow: (%q,%q) vs (%q,%q)",
st.Name, st.ResolvedModel, st.ResolvedHop, st.Model, st.EscalateTo)
}
}
// …and the INVARIANT still bites: an endpoint that may not receive the label is refused, which is the
// only thing an allow policy does.
incapable := strings.Replace(allowOnly, "model: capable-model", "model: plain-model", 1)
_, loadBad := labelFixture(t, incapable)
pb, err := loadBad([]string{"restricted-synth"})
if err != nil {
t.Fatalf("load: %v", err)
}
rerr := pb.ContentRoutingError()
if rerr == nil || !strings.Contains(rerr.Error(), "may not receive content label") {
t.Fatalf("action=allow must still refuse an incapable endpoint — the invariant IS the policy, got %v", rerr)
}
}
// TestAllowActionRejectsRoutingKnobs pins that an allow policy refuses the knobs it cannot honour, rather
// than ignoring them: an author who wrote a chain or a per-stage model meant something the engine will
// not do, and silently dropping it is how a config comes to lie about its own routing.
func TestAllowActionRejectsRoutingKnobs(t *testing.T) {
base := `core: C1
version: 1
defaults: { max_output_ratio: 2, min_max_tokens: 512 }
content_policy:
- { label: restricted-synth, action: allow%s }
stages:
- name: draft
role: translator
model: capable-model
prompt_override: @PROMPT@
prompt_version: v
escalate_to: capable-hop%s
escalation:
chains: { restricted: [second-model] }
budget_usd: 1.0
`
cases := []struct{ name, policyTail, stageTail, want string }{
{"chain on an allow policy", ", chain: restricted", "", "takes no chain"},
{"label_models for an allow label", "", "\n label_models: { restricted-synth: second-model }", "is meaningless"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
_, load := labelFixture(t, fmt.Sprintf(base, c.policyTail, c.stageTail))
if _, err := load(nil); err == nil || !strings.Contains(err.Error(), c.want) {
t.Fatalf("want a load error mentioning %q, got %v", c.want, err)
}
})
}
}
// TestRatifiedLabelPairIsDataOnly is the delta's acceptance shape, written as the owner ratified it: TWO
// labels, one that only ASSERTS (its providers are already correct, so no model moves) and one that
// RE-ROUTES (its endpoints differ). Both values here are synthetic — the point is that the SHAPE needs no
// Go change, so whichever vocabulary the data ends up carrying, the mechanism already holds it.
func TestRatifiedLabelPairIsDataOnly(t *testing.T) {
pair := `core: C1
version: 1
defaults: { max_output_ratio: 2, min_max_tokens: 512 }
content_policy:
- { label: restricted-synth, action: allow }
- { label: second-synth, action: route, chain: rerouted }
stages:
- name: draft
role: translator
model: capable-model
prompt_override: @PROMPT@
prompt_version: v
escalate_to: capable-hop
label_models: { second-synth: narrowed-model }
- name: edit
role: editor
model: capable-model
prompt_override: @PROMPT@
prompt_version: v
escalation:
chains: { rerouted: [second-model] }
budget_usd: 1.0
`
_, load := labelFixture(t, pair)
// (a) the asserting label alone: nothing moves, no chain/budget needed by it.
only, err := load([]string{"restricted-synth"})
if err != nil {
t.Fatalf("load: %v", err)
}
if err := only.ContentRoutingError(); err != nil {
t.Fatalf("restricted-synth must be runnable on capable endpoints: %v", err)
}
if only.Stages[0].ResolvedModel != "capable-model" || only.Stages[0].ResolvedHop != "capable-hop" {
t.Errorf("the asserting label must not move the route, got (%q,%q)", only.Stages[0].ResolvedModel, only.Stages[0].ResolvedHop)
}
// (b) BOTH labels: the re-routing one moves the draft leg, and the invariant is applied to the union —
// narrowed-model accepts only one of the two synthetic labels, so it must be refused here.
both, err := load([]string{"restricted-synth", "second-synth"})
if err != nil {
t.Fatalf("load: %v", err)
}
rerr := both.ContentRoutingError()
if rerr == nil || !strings.Contains(rerr.Error(), "narrowed-model") {
t.Fatalf("the invariant must be applied to the UNION of the book's labels (narrowed-model accepts only one), got %v", rerr)
}
if both.Stages[0].ResolvedModel != "narrowed-model" {
t.Errorf("the re-routing label must still resolve its stage, got %q", both.Stages[0].ResolvedModel)
}
// (c) the pair on endpoints that accept BOTH: runnable, one leg re-routed, the other untouched.
ok := strings.Replace(pair, "label_models: { second-synth: narrowed-model }", "label_models: { second-synth: second-model }", 1)
ok = strings.Replace(ok, "chains: { rerouted: [second-model] }", "chains: { rerouted: [capable-hop] }", 1)
_, loadOK := labelFixture(t, ok)
p, err := loadOK([]string{"restricted-synth", "second-synth"})
if err != nil {
t.Fatalf("load: %v", err)
}
if err := p.ContentRoutingError(); err != nil {
t.Fatalf("the ratified PAIR shape must be runnable on capable endpoints with zero Go changes: %v", err)
}
if p.Stages[0].ResolvedModel != "second-model" || p.Stages[0].ResolvedHop != "capable-hop" {
t.Errorf("draft resolved (%q,%q), want (second-model,capable-hop)", p.Stages[0].ResolvedModel, p.Stages[0].ResolvedHop)
}
if p.Stages[1].ResolvedModel != "capable-model" {
t.Errorf("the edit stage carries no route for either label, so it stays configured, got %q", p.Stages[1].ResolvedModel)
}
}

View file

@ -67,9 +67,21 @@ type LabelPolicy struct {
}
const (
// LabelActionRoute / LabelActionTerminal are the two policy SHAPES the engine implements. They are
// actions, not content classes — no label value appears here.
// The three policy SHAPES the engine implements. They are actions, not content classes — no label
// value appears here, and ALL THREE carry the capability invariant (every model a labelled run can
// call must accept every label of the book); they differ only in what else they do.
//
// route — re-route the claimed stages to their label_models and take the single hop from `chain`.
// allow — process on capable endpoints ONLY, changing no route: the invariant is the whole policy.
// terminal — do not process the book at all (an entry refusal by declared data, not a screen).
//
// `allow` exists because the invariant is the point of a label that needs no different model: with only
// route/terminal a label whose providers are already correct was inexpressible — route demanded a
// chain, a budget and a per-stage model it has no use for, and terminal refuses the book outright. A
// pair of labels where one re-routes and the other only asserts is exactly the ratified vocabulary
// shape, so without this value the mechanism failed its own test ("a second label is added by DATA").
LabelActionRoute = "route"
LabelActionAllow = "allow"
LabelActionTerminal = "terminal"
)
@ -186,11 +198,13 @@ type Stage struct {
PromptVersion string `yaml:"prompt_version"`
Temperature float64 `yaml:"temperature"`
Reasoning string `yaml:"reasoning"` // "", off, low, medium, high
// ReasoningMaxTokens is the explicit reasoning-token BUFFER reserved for this stage
// when it thinks (reasoning low|medium|high) on an ADDITIVE-billing provider (xAI —
// reasoning bills on top of completion, D13.6). Required (>0) in exactly that case
// (fail-fast in LoadPipeline); ignored for subset providers / reasoning-off. It only
// sizes the reservation (EstimateUSD), so it is NOT part of the snapshot/wire.
// ReasoningMaxTokens is the explicit reasoning-token BUFFER reserved for this stage on an
// ADDITIVE-billing provider (xAI — reasoning bills on top of completion, D13.6). Required (>0)
// whenever the stage's RESOLVED model sits on such a provider — INCLUDING at reasoning:"off"
// (D39.26 добор B, form 2): "off" suppresses thinking only where the capability carries an
// off-switch, so exempting it was a silent exit that left the ceiling blind. Ignored for subset
// providers. A call that does suppress thinking merely over-reserves — the ceiling tightens, it never
// goes blind. It only sizes the reservation (EstimateUSD), so it is NOT part of the snapshot/wire.
ReasoningMaxTokens int `yaml:"reasoning_max_tokens"`
// EscalateTo is the SINGLE-HOP fallback model tried ONCE when this stage's
// output is a deterministic content-failure another model might fix (echo /
@ -448,12 +462,18 @@ func (p *Pipeline) resolveContentRouting(models *Models, labels []string, bad fu
case len(chain) > 1:
bad("content_policy[%d] (%s): chain %q has %d members — MULTI-HOP IS NOT BUILT (one hop = chain[0], D39.26 point 3). Shorten the chain to its head, or keep the extra members in a chain no policy references", i, pol.Label, pol.Chain, len(chain))
}
case LabelActionAllow:
// Nothing is re-routed, so a chain would be a fallback nobody can reach: refused rather than
// ignored, because an author who wrote one meant something the engine will not do.
if pol.Chain != "" {
bad("content_policy[%d] (%s): action=%s takes no chain — it changes no route, so there is no hop to take. Use action=%s if this label must re-route, or drop the chain", i, pol.Label, LabelActionAllow, LabelActionRoute)
}
case LabelActionTerminal:
if pol.Chain != "" {
bad("content_policy[%d] (%s): action=terminal takes no chain (nothing is ever called for such a book)", i, pol.Label)
}
default:
bad("content_policy[%d] (%s): action must be %s|%s, got %q", i, pol.Label, LabelActionRoute, LabelActionTerminal, pol.Action)
bad("content_policy[%d] (%s): action must be %s|%s|%s, got %q", i, pol.Label, LabelActionRoute, LabelActionAllow, LabelActionTerminal, pol.Action)
}
}
// label_models may only name models that exist, whether or not this book activates them: a typo in
@ -469,8 +489,11 @@ func (p *Pipeline) resolveContentRouting(models *Models, labels []string, bad fu
if _, ok := models.Models[model]; !ok {
bad("stage %q: label_models[%q] model %q is not defined in models.yaml", st.Name, label, model)
}
if p.policyFor(label) == nil {
switch pol := p.policyFor(label); {
case pol == nil:
bad("stage %q: label_models[%q] has no content_policy entry — a route the registry never mentions cannot be taken", st.Name, label)
case pol.Action != LabelActionRoute:
bad("stage %q: label_models[%q] is meaningless — that label's policy is action=%s, which changes no route (only action=%s re-routes stages). Drop the entry, or change the policy", st.Name, label, pol.Action, LabelActionRoute)
}
}
}

View file

@ -572,3 +572,80 @@ func TestEditorOnlyLabelLeavesTheDraftWaveUntouched(t *testing.T) {
t.Errorf("the EDIT-wave snapshot must move (%.12s) — the editor is called on another model, so its checkpoints are not the same calls", labelledEdit)
}
}
// TestAssertOnlyLabelRunsWithoutMovingAnything drives the third policy shape through the REAL driver: a
// book whose label only ASSERTS must run exactly like the unlabelled one — same wire, same snapshots,
// same money — while the capability invariant still guards every call. It needs no escalation budget and
// no chain, which is the whole reason the shape exists.
func TestAssertOnlyLabelRunsWithoutMovingAnything(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
plain := newRunner(t, setupEscalationProject(t, srv.URL, 0, nil))
plainDraft, _, err := plain.snapshotIDForWave(waveDraft)
if err != nil {
t.Fatal(err)
}
plainEdit, _, err := plain.snapshotIDForWave(waveEdit)
if err != nil {
t.Fatal(err)
}
plainRes, err := plain.TranslateBook(context.Background())
if err != nil {
t.Fatalf("unlabelled translate: %v", err)
}
plainBodies := append([]string(nil), rec.all()...)
plain.Close()
// Same project, plus a label whose policy only asserts (no chain, no label_models, budget 0).
rec2 := &reqRec{}
srv2 := newJSONProvider(rec2, draftEdit)
defer srv2.Close()
r := newRunner(t, setupEscalationProject(t, srv2.URL, 0, &labelRouting{
acceptsLabels: []string{synthLabel},
bookLabels: []string{synthLabel},
policy: synthLabel,
action: "allow",
}))
defer r.Close()
res, err := r.TranslateBook(context.Background())
if err != nil {
t.Fatalf("an asserting label must not disturb the run: %v", err)
}
if res.Chunks[0].Disposition != DispOK || res.TotalUSD != plainRes.TotalUSD {
t.Errorf("run diverged: disposition=%s cost=%v want ok/%v", res.Chunks[0].Disposition, res.TotalUSD, plainRes.TotalUSD)
}
// The wire must be identical body for body — an assertion is not a wire change.
bodies := rec2.all()
if len(bodies) != len(plainBodies) {
t.Fatalf("call count changed: %d vs %d", len(bodies), len(plainBodies))
}
for i := range bodies {
if bodies[i] != plainBodies[i] {
t.Errorf("call %d body differs under an asserting label:\n got %s\nwant %s", i, bodies[i], plainBodies[i])
}
}
// …and so must both snapshots: an asserting label moves NO route, so it can re-bill nothing.
draft, _, err := r.snapshotIDForWave(waveDraft)
if err != nil {
t.Fatal(err)
}
edit, _, err := r.snapshotIDForWave(waveEdit)
if err != nil {
t.Fatal(err)
}
if draft != plainDraft || edit != plainEdit {
t.Errorf("an asserting label moved a snapshot (draft %.12s→%.12s, edit %.12s→%.12s) — it changes no model, so it must re-bill nothing",
plainDraft, draft, plainEdit, edit)
}
// The invariant is still the point: strip the capability and the SAME book must refuse to run.
incapable := setupEscalationProject(t, srv2.URL, 0, &labelRouting{
bookLabels: []string{synthLabel},
policy: synthLabel,
action: "allow",
})
if _, err := NewRunner(incapable, obs.NewLogger()); err == nil || !strings.Contains(err.Error(), "may not receive content label") {
t.Fatalf("an asserting label must still refuse incapable endpoints, got %v", err)
}
}

View file

@ -1080,7 +1080,8 @@ func TestRunnerRejectsCoverageGateWithoutThresholds(t *testing.T) {
type labelRouting struct {
acceptsLabels []string // provider accepts_labels
bookLabels []string // book content_labels
policy string // the label the registry routes (empty = no registry entry written)
policy string // the label the registry declares (empty = no registry entry written)
action string // the policy action ("" = route; "allow" asserts without re-routing)
labelModel string // the draft stage's label_models target for `policy`
editModel string // the EDIT stage's label_models target (the editor-only routing case)
chainModel string // the head of the label's chain (its single hop)
@ -1125,16 +1126,25 @@ models:
labelModelsLine, editLabelModelsLine, policyBlock := "", "", ""
if lbl != nil && lbl.policy != "" {
if lbl.labelModel != "" {
labelModelsLine = fmt.Sprintf(", label_models: { %s: %s }", lbl.policy, lbl.labelModel)
action := lbl.action
if action == "" {
action = "route"
}
if lbl.editModel != "" {
editLabelModelsLine = fmt.Sprintf(", label_models: { %s: %s }", lbl.policy, lbl.editModel)
if action == "route" {
if lbl.labelModel != "" {
labelModelsLine = fmt.Sprintf(", label_models: { %s: %s }", lbl.policy, lbl.labelModel)
}
if lbl.editModel != "" {
editLabelModelsLine = fmt.Sprintf(", label_models: { %s: %s }", lbl.policy, lbl.editModel)
}
policyBlock = fmt.Sprintf("content_policy:\n - { label: %s, action: route, chain: labelled }\n", lbl.policy)
} else {
// An asserting policy takes no chain and no per-stage route — writing either is a load error.
policyBlock = fmt.Sprintf("content_policy:\n - { label: %s, action: %s }\n", lbl.policy, action)
}
policyBlock = fmt.Sprintf("content_policy:\n - { label: %s, action: route, chain: labelled }\n", lbl.policy)
}
chainBlock := ""
if lbl != nil && lbl.chainModel != "" {
if lbl != nil && lbl.chainModel != "" && (lbl.action == "" || lbl.action == "route") {
chainBlock = fmt.Sprintf(" chains: { labelled: [%s] }\n", lbl.chainModel)
}
writeFile(t, filepath.Join(dir, "pipeline.yaml"), fmt.Sprintf(`

View file

@ -1,6 +1,6 @@
# Промт бэкенд-сессии: ПАК-17 «канал B (18+) вживую» (ДВУХФАЗНЫЙ: дизайн → ратификация → стройка)
**Статус: ФАЗА 1 ИСПОЛНЕНА И РАТИФИЦИРОВАНА (`D39.26`, 25.07) → ФАЗА 2 РАЗМЕЧЕНА И ОТКРЫТА.** Дизайн-отчёт `docs/archive/reports/PACK17_DESIGN_2026-07-25.md` принят **В РЕДАКЦИИ §14** (тело §013 читать ТОЛЬКО через §14; при расхождении побеждает §14) + ревью-шапка оркестратора. **Фаза 2 стартует по §«ФАЗА 2» ниже** — там ратифицированный «ровно так»-список; §«ФАЗА 1» оставлена как история задания.
**Статус: ✅ ПАК ОТРАБОТАН ЦЕЛИКОМ И ЗАКРЫТ (дизайн `D39.26` · стройка `D39.27` · дельта `D39.28`, 25.07). ПРОМТ АРХИВНЫЙ — исполнять из него нечего.** Отчёты с ревью-шапками: `docs/archive/reports/PACK17_{DESIGN,CHANNEL_B_REPORT,DELTA}_2026-07-25.md` (дизайн принят **В РЕДАКЦИИ §14**). Остаток пака — не код: ФАКТЫ ToS под `accepts_labels` (зона полигона, `experiments/00-provider-quirks.md`) · Q2 редактор и D22.7 перед первой explicit-книгой. Всё ниже — история задания.
**Статус фазы 1: ВЫДАН 25.07.2026, оркестратор №7.** Цель: 18+ контент (уровни 12) идёт через волновой стек по adult-каналу — permissive-провайдеры, refusal-обработка, механические гарантии маршрутизации. Ратифицированная база: D14 (состав канала B) · D19 (Mistral = переводчик канала B; register-оговорка эха D19.1; судьи 18+ п.6: Gemini fail-closed на эротике, Grok первичный) · D22 (развязки 18+/эха; L3-спека D22.7) · D4 (adult-цепочка = permissive-провайдеры; уже в конфигах: `escalation.chains.adult: [grok-4.3]`). Эмпирика полигона: `experiments/10-explicit-benchmark` (violence) · `11-erotica-benchmark`.
@ -33,7 +33,9 @@
## ✅ ФАЗА 2 ИСПОЛНЕНА, ПРИНЯТА И ЗАЛЕНДЕНА (`D39.27`, 25.07) — ниже история задания; АКТИВНА ТОЛЬКО «ДЕЛЬТА» в конце файла
## ДЕЛЬТА ПОСЛЕ ПРИЁМКИ (ответ владельца по вокабуляру — единственная открытая работа пака-17)
## ✅ ДЕЛЬТА ИСПОЛНЕНА И ПРИНЯТА (`D39.28`, 25.07) — ПАК-17 ЗАКРЫТ ЦЕЛИКОМ; промт архивный, исполнять из него нечего
## ДЕЛЬТА ПОСЛЕ ПРИЁМКИ (история задания)
**Владелец выбрал ПАРУ лейблов `violence` / `sexually-explicit`** (сохраняет ратифицированную развязку D14 п.1: DeepSeek запрещён только на sexually explicit, на violence допустим ⇒ violence-книга не теряет dspro-редактора). Из этого — одна обязательная правка механизма и одна операционная:

File diff suppressed because one or more lines are too long

View file

@ -11,15 +11,15 @@
## Структура
- `architecture/` — синтез. **Источник истины по решениям — [`05-decisions-log.md`](architecture/05-decisions-log.md) (D1D39.27); при конфликте с любым доком он выше.**
- `architecture/` — синтез. **Источник истины по решениям — [`05-decisions-log.md`](architecture/05-decisions-log.md) (D1D39.28); при конфликте с любым доком он выше.**
- `01-decisions.md` — принципы Р1Р10; `02-mvp-plan.md` — фазы и приёмка (v3, 09.07); `03-implementation-notes.md` — контракты Фазы 0; `04-unhappy-paths.md` — ~70 режимов отказа → механизм; `06-memory-risk-registry.md` — реестр рисков банка памяти; **[`09-target-architecture.md`](architecture/09-target-architecture.md) — целевая 7-слойная архитектура (D39; статус стройки — шапка-таблица; инвариант общности §0.1)** · [`10-prompt-architecture.md`](architecture/10-prompt-architecture.md) — консолидированная промпт-заметка (концерн 4) · [`12-go-style-notes.md`](architecture/12-go-style-notes.md) — норматив общности §0 + Go-ответы. **Исполненные (архив 25.07, санкция владельца, → `archive/architecture/`):** [`07-strategic-review.md`](archive/architecture/07-strategic-review.md) (стратаудит 09.07 — курс исполнен) · [`08-sync-audit-ledger.md`](archive/architecture/08-sync-audit-ledger.md) (ледджер синк-аудита — отработан) · [`11-implementation-plan.md`](archive/architecture/11-implementation-plan.md) (план пака-11 — исполнен целиком); `components.puml`/`pipeline.puml` — диаграммы (перерисованы 25.07 под пост-пак-16 реальность: волновой исполнитель, пакетный сплит пака-15, repair-петля за `enabled:false`, generic content-labels D39.25; владелец смотрит PlantUML-расширением VS Code; вручную НЕ рендерить).
- `experiments/` — эмпирика «Полигона»: `00-provider-quirks` (читать перед любым вызовом провайдера), `01-token-calibration`, `02-refusal-benchmark`, `03-local-stand`, `04-editor-quality`, `06-local-extraction`, `07-coverage-precision`, `08-cost-model-v2` (актуальная денежная модель), `09-pilot-protocol` (пилот Ф2.5 + поправки D13), `10-explicit-benchmark` (18+ violence-рука канала B), `11-erotica-benchmark` (erotica по трём парам/регистрам — закрытие D14.4, D22), `12-quality-diagnosis`/`13-translator-bakeoff`/`14-quality-empirics`/`14b-meaning-battery` (дуга качества «мерить→строить», закрыты → D30/D32/D37/D38), `15-segmentation-empirics` (exp15: когезия под floor-шумом, фертильность — закрыт → D39.7/D39.8), `16-bank-mining` (exp16: WHICH/WHAT банк-майнинга — закрыт → D39.10).
- `research/` — фактура исследований 0405.07: `0110` базовые, `11-gap-*` добор критиком, `12-*` режимы отказа/отзывы/таксономии (+ два внешних материала с провенанс-шапками), `13` валидация памяти, `14` адаптивная память, `15` голос и состояние (принят, D21), **`16` ридер-IDE (принят с ревью-шапкой, D29)**, **`17` внешняя критика GPT-5.6 (принят с ревью-шапкой, D25)** — у 16/17 читать шапку прежде тела. **`18` рычаги качества (два отчёта, D36)** · **`19` нарезка+когезия+контракт t/e (D39.1)** · **`20` банк-майнинг W1.5 (D39.6)** · **`21` обзор LLM-транспорта чужих харнессов (23.07: наш транспорт опережает/вровень со всеми 11)** · **`22` доменные харнессы перевода (24.07: калибрующий — ядро подтверждено, впереди COGS/18+/общность/измеренность, позади Q4-выпуск и gate+repair-петля; сиквел `02`/`05`)** — у всех ревью-шапки. ⚠ Часть под superseded-баннерами (01/02/03/04/05/09 и gap-1/2/5) — **читай баннер прежде содержимого**.
- `PROGRESS.md`**журнал** (CURRENT-STATE сверху, ниже хронология; НЕ источник решений).
- **Активные хендофф-промты (пост-пак-16, 25.07):** [`BACKEND_PACK17_CHANNEL_B_SESSION_PROMPT.md`](BACKEND_PACK17_CHANNEL_B_SESSION_PROMPT.md) (**пак-17 канал B (18+) вживую — ЗАЛЕНДЕН ЦЕЛИКОМ 25.07: фаза 1 дизайн D39.26, фаза 2 стройка D39.27; АКТИВНА только §«ДЕЛЬТА» — assert-only `action` под ратифицированный ПАРНЫЙ вокабуляр `violence`/`sexually-explicit`**; отчёты с ревью-шапками — [`archive/reports/PACK17_DESIGN_2026-07-25.md`](archive/reports/PACK17_DESIGN_2026-07-25.md), [`archive/reports/PACK17_CHANNEL_B_REPORT_2026-07-25.md`](archive/reports/PACK17_CHANNEL_B_REPORT_2026-07-25.md); L3 — жёсткая линия, live только по санкции владельца) · [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (хендофф №6, 25.07 — только живое: роль/методология/уроки/нормы §111; состояние НЕ дублирует) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual пилот/18+/echo). **Пак-16 ЗАЛЕНДЁН 25.07** (слой-3 diff-редактор: двухфазный дизайн D39.24 → стройка за `enabled:false`; отчёты с ревью-шапками — [`archive/reports/PACK16_DESIGN_2026-07-25.md`](archive/reports/PACK16_DESIGN_2026-07-25.md), [`archive/reports/PACK16_DIFF_EDITOR_REPORT_2026-07-25.md`](archive/reports/PACK16_DIFF_EDITOR_REPORT_2026-07-25.md)). **Паки 12/13/14/15 ЗАЛЕНДЕНЫ 2425.07** (приёмка воркфлоу-линзами; отчёты с ревью-шапками — [`archive/reports/PACK14_GENERALITY_REPORT_2026-07-24.md`](archive/reports/PACK14_GENERALITY_REPORT_2026-07-24.md), [`archive/reports/PACK15_STRUCTURE_REPORT_2026-07-25.md`](archive/reports/PACK15_STRUCTURE_REPORT_2026-07-25.md); норматив — [`architecture/12-go-style-notes.md`](architecture/12-go-style-notes.md)). Дальше: **пак-17 = канал B вживую** (батч со следующим платным прогоном: вопросы итерации №2 + замер остатка ремонта → решение о включении петли) → голос/состояние D21 + C2/C3 → пилот Ф2.5. Закрытые — в `archive/prompts/`.
- **Активные хендофф-промты (пост-пак-16, 25.07):** [`BACKEND_PACK17_CHANNEL_B_SESSION_PROMPT.md`](BACKEND_PACK17_CHANNEL_B_SESSION_PROMPT.md) (**пак-17 канал B (18+) вживую — ОТРАБОТАН ЦЕЛИКОМ 25.07: дизайн D39.26 · стройка D39.27 · дельта `allow` D39.28; вокабуляр = ПАРА `violence`/`sexually-explicit`, остаток — ФАКТЫ ToS в зоне полигона**; отчёты с ревью-шапками — [`archive/reports/PACK17_DESIGN_2026-07-25.md`](archive/reports/PACK17_DESIGN_2026-07-25.md), [`archive/reports/PACK17_CHANNEL_B_REPORT_2026-07-25.md`](archive/reports/PACK17_CHANNEL_B_REPORT_2026-07-25.md), [`archive/reports/PACK17_DELTA_2026-07-25.md`](archive/reports/PACK17_DELTA_2026-07-25.md); L3 — жёсткая линия, live только по санкции владельца) · [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (хендофф №6, 25.07 — только живое: роль/методология/уроки/нормы §111; состояние НЕ дублирует) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual пилот/18+/echo). **Пак-16 ЗАЛЕНДЁН 25.07** (слой-3 diff-редактор: двухфазный дизайн D39.24 → стройка за `enabled:false`; отчёты с ревью-шапками — [`archive/reports/PACK16_DESIGN_2026-07-25.md`](archive/reports/PACK16_DESIGN_2026-07-25.md), [`archive/reports/PACK16_DIFF_EDITOR_REPORT_2026-07-25.md`](archive/reports/PACK16_DIFF_EDITOR_REPORT_2026-07-25.md)). **Паки 12/13/14/15 ЗАЛЕНДЕНЫ 2425.07** (приёмка воркфлоу-линзами; отчёты с ревью-шапками — [`archive/reports/PACK14_GENERALITY_REPORT_2026-07-24.md`](archive/reports/PACK14_GENERALITY_REPORT_2026-07-24.md), [`archive/reports/PACK15_STRUCTURE_REPORT_2026-07-25.md`](archive/reports/PACK15_STRUCTURE_REPORT_2026-07-25.md); норматив — [`architecture/12-go-style-notes.md`](architecture/12-go-style-notes.md)). Дальше: **пак-17 = канал B вживую** (батч со следующим платным прогоном: вопросы итерации №2 + замер остатка ремонта → решение о включении петли) → голос/состояние D21 + C2/C3 → пилот Ф2.5. Закрытые — в `archive/prompts/`.
- `archive/``prompts/`: закрытые сессионные промты (только история, инструкции оттуда не исполнять) · `reports/`: отчёты паков/ресёрч-сессий с ревью-шапками (живая фактура приёмок — на них ссылаются D-лог и активные промты) · `architecture/`: исполненные арх-доки (07 стратревью · 08 синк-ледджер · 11 план пака-11) · три слайса хроники `PROGRESS-2026-07-{04-10,10-13,13-25}.md`.
## Статус (2026-07-25, пост-пак-17 — механизм content-labels построен и залендён, D39.27 — механизация выпускного качества)
## Статус (2026-07-25, пак-17 ЗАКРЫТ ЦЕЛИКОМ — механизм content-labels живой, D39.26D39.28 — механизация выпускного качества)
Фаза 0 ✅; Ф1-инфра ✅ (D20D28). **АРХ-РЕСЕТ D39 ИСПОЛНЕН ЦЕЛИКОМ:** 7-слойная архитектура (`09-*`) → исследовательская программа (D39.1D39.10) → план (D39.12, `11-*`) → стройка пака-11 (D39.13D39.19: волновой исполнитель · чанкер output-бюджет · src→dst-редактор · Go-майнер паритет-EXACT · банкнота · `internal/lang`+langpacks · reject-set · echo-сплит). **ПЕРЕ-ПРОГОН rerun2 ИСПОЛНЕН И ПРОЧИТАН (D39.20, 23.07):** операционка вся подтверждена живьём (ОДИН resnapshot · **$0-резюм драфта ×3 арма** · echo 0 · $6.63<$15 · майнинг-стоп→подпись→reject-set); **планка ≤2 НЕ пройдена** — но дефект-классы полностью механизируемы. Три сигнала (судья dspro 0.679>glm 0.589>>mistral 0.232, floor-шум 0 · два слепых чтения): **анти-корреляция гладкость↔верность доказана трижды**; **mistral исключён как несущий редактор** (голос = north-star промпта); dspro-vs-glm → итерация №2 после механизации (драфт $0). research/21: транспорт наш опережает/вровень со всеми 11, рефакторинг отвергнут. **Эксп ЗАКРЫТ (D39.22), итерация №2 отложена; редактор = deepseek-v4-pro ИНТЕРИМ (glm-резерв); стайл-каноны D39.21 ратифицированы.** **Очередь (курс — разработка бэкенда):** паки 1216 залендены (транспорт-гигиена · выпускной QA · генеральность · структура: движок разложен `internal/{text,seed,chunk,miner,checks,membank}`+композит-корень, конфиг-слой пары, строгий декод · слой-3 diff-редактор ПОСТРОЕН за `enabled:false` по двухфазному дизайну D39.24 — позиционная детекция + read-only скан остатка + машинерия ремонта §15.2; включение платного пути = решение владельца по замеру остатка на пост-профилактическом прогоне) → пак-17 = канал B вживую (**ЗАЛЕНДЕН ЦЕЛИКОМ: D39.26 дизайн + D39.27 стройка; вокабуляр = ПАРА `violence`/`sexually-explicit`, хвост — assert-only `action`**; D39.25: в движке НЕТ понятия «18+» — generic content-labels × provider-capabilities; лейблы вне хешей · резолв до валидации · один хоп `chain[0]` · гранулярность = книга; на владельце — вокабуляр лейблов и редактор под лейблом; батч со следующим платным прогоном: вопросы итерации №2 + замер остатка ремонта) → сид-дельта (元/赤城/学堂家老/春秋蝉) → Ф2-механизмы (D21 голос/состояние, native-Gemini судья) + ja→ru §B5 → пилот Ф2.5 (блокеры прежние: билингв-якорь D25.9-Q1, корпус, судья-дублёр D22.6). Судья 18+ = grok-фолбэк (квирк D22.6 подтверждён живьём). Ключ xAI: единый, data-sharing, off перед продом (D27).

File diff suppressed because one or more lines are too long

View file

@ -18,7 +18,7 @@
> | 6 Export | ✅ ПОСТРОЕН: `checks.ExportNormalize` на каждом финале; санитайзер-классы strip+export | D39.2 · D38.3 |
> | 7 Гигиена | ЧАСТИЧНО: реестр role→инъекция · вынос `foldModelWire` · снос сирот/`gatesEnabled` — сделаны; **target-aware гейты — ОТКРЫТО** (ru-target-долг = ограничение, D39.24 §15.3; едет с канал-B/таргет-работой) | D39.2 · паки 1116 |
>
> §1 «как есть» читать как снимок ДО стройки (const-чанкер · glm-редактор · мёртвые `STMDepth`/`OverlapTokens` — всего этого больше нет). §4 отработан целиком: трек B исполнен (exp15/exp16 — программа ЗАВЕРШЕНА, D39.7/D39.10), гейт пере-прогона пройден фактом rerun2 (D39.20; планка ≤2 НЕ пройдена, эксп ЗАКРЫТ D39.22, редактор = deepseek-v4-pro интерим). Канал B — пак-17 **ЗАЛЕНДЕН ЦЕЛИКОМ (дизайн D39.26 + стройка D39.27: механизм лейблов живой, golden байт-идентичен, `permissive`/`Stage.Channel`/D4.1-блок отставлены, `failover.go` удалён; вокабуляр = ПАРА `violence`/`sexually-explicit`, хвост — assert-only `action`)**; дизайн РАТИФИЦИРОВАН D39.26 (лейблы вне хешей — назначенный слой D20.2-Q3 = резолвнутая модель стадии пер-стадийно/пер-волново · резолв ДО валидации, «достижимое» лейбл-зависимо · один хоп `chain[0]` · гранулярность = книга · `content_labels`(книга) ⊆ `accepts_labels`(модель) · `Stage.Channel`/`permissive`/D4.1-блок отставляются одним диффом), фаза 2 = стройка; инженерная форма переопределена **D39.25**: в движке НЕТ понятия «18+» — generic content-labels × provider-capabilities, refusal причино-агностичен; «18+ линия» в §6 читать как продукт-линию, не механизм движка.
> §1 «как есть» читать как снимок ДО стройки (const-чанкер · glm-редактор · мёртвые `STMDepth`/`OverlapTokens` — всего этого больше нет). §4 отработан целиком: трек B исполнен (exp15/exp16 — программа ЗАВЕРШЕНА, D39.7/D39.10), гейт пере-прогона пройден фактом rerun2 (D39.20; планка ≤2 НЕ пройдена, эксп ЗАКРЫТ D39.22, редактор = deepseek-v4-pro интерим). Канал B — пак-17 **ЗАКРЫТ ЦЕЛИКОМ (дизайн D39.26 + стройка D39.27 + дельта D39.28: механизм лейблов живой, действия `route|allow|terminal` с одним инвариантом, golden байт-идентичен, `permissive`/`Stage.Channel`/D4.1-блок отставлены, `failover.go` удалён; вокабуляр = ПАРА `violence`/`sexually-explicit`; `accepts_labels` пусты до ФАКТОВ ToS — fail-closed)**; дизайн РАТИФИЦИРОВАН D39.26 (лейблы вне хешей — назначенный слой D20.2-Q3 = резолвнутая модель стадии пер-стадийно/пер-волново · резолв ДО валидации, «достижимое» лейбл-зависимо · один хоп `chain[0]` · гранулярность = книга · `content_labels`(книга) ⊆ `accepts_labels`(модель) · `Stage.Channel`/`permissive`/D4.1-блок отставляются одним диффом), фаза 2 = стройка; инженерная форма переопределена **D39.25**: в движке НЕТ понятия «18+» — generic content-labels × provider-capabilities, refusal причино-агностичен; «18+ линия» в §6 читать как продукт-линию, не механизм движка.
---

View file

@ -0,0 +1,64 @@
# ПАК-17, ДЕЛЬТА ПОСЛЕ ПРИЁМКИ: третье значение `action` под ратифицированную ПАРУ лейблов
> **Ревью-шапка оркестратора №8 (25.07, приёмка execute-first, инлайн; ПРИНЯТА и залендена — `D39.28`; пак-17 закрыт целиком). 0 блокеров.** Ре-ранил сам: build/vet чисты · `gofmt` чист (единственная жалоба `internal/llm/llm.go` воспроизводится на HEAD — сессия файл не трогала, оговорка честная) · **`-race` 12/12 ok** · **golden `f7641993…4f0020f8` = HEAD байт-в-байт**, `testdata/` чист · **парити EXACT `n=13618 … 0.9655 (56/58)`** · **ценз диффом ИМЁН: 451→455, удалено 0** — ровно четыре названных теста.
> **Моя независимая мутация:** инвариант проверяет только ПЕРВЫЙ лейбл вместо объединения → красный `TestRatifiedLabelPairIsDataOnly`, то есть союзная семантика ратифицированной ПАРЫ запинена именно тем тестом, который кодирует форму ответа владельца. **Сквозняк `TestAssertOnlyLabelRunsWithoutMovingAnything` прочитан — не вакуумный:** число вызовов + КАЖДОЕ тело запроса + стоимость + ОБА волновых снапшота, и всё при `budget_usd = 0`.
> **Ратифицировано:** имя `allow` · «все три действия несут один инвариант, различаются остальным» · запрет `chain`/`label_models` при `allow` как громкая ошибка · две объявленные правки фикстур (новая корректная семантика) · пустые `accepts_labels` как ВЕРНОЕ fail-closed состояние (проверил диффы данных: ни одного ToS-утверждения от сессии, вокабуляр в шиппинг не прибит). Фикс-лист D39.27 закрыт.
> **Статус: ИСПОЛНЕНО, СДАНО НА ПРИЁМКУ. Сессия НЕ коммитила.** База: `D39.27` (приёмка фазы 2) + §«ДЕЛЬТА ПОСЛЕ ПРИЁМКИ» промта пака-17. Дельта закрывает единственную открытую работу пака: владелец выбрал ПАРУ `violence` / `sexually-explicit`, а чистого capability-ассерта в enum действий не было — то есть ратифицированный вокабуляр был НЕВЫРАЗИМ данными. `git mv` не применялся, удалений нет.
## §1. Что сделано (три пункта дельты)
**1. Третье значение `action``allow` (`internal/config/pipeline.go`).** Имя выбрал приорное: `route | allow | terminal` читается как естественная тройка, а альтернативы отверг — `assert` ничего не говорит оператору, `require-capability` многословен для ключа данных. Семантика вынесена в док-комментарий enum'а: **все три действия несут ОДИН инвариант** (каждая достижимая модель обязана принимать все лейблы книги) и различаются только остальным:
| action | инвариант | маршрут | требует |
|---|---|---|---|
| `route` | да | пере-маршрутизирует заявленные стадии + один хоп `chain[0]` | `chain` (ровно 1 член) · `budget_usd > 0` · ≥1 стадия с `label_models` |
| **`allow`** | **да — это ВЕСЬ смысл политики** | **не меняет ничего** | **ничего; `chain`/`label_models` ЗАПРЕЩЕНЫ как бессмысленные** |
| `terminal` | н/д (ни одного вызова) | — | ничего |
Дельта в коде вышла ровно такой, как предсказал промт: значение enum + ветка валидации (`chain` при `allow` — громко) + ветка в проверке `label_models` (запись для не-route лейбла — громко, с указанием, что менять) + примеры. `checkContentRouting` править не пришлось — он уже был action-агностичен (кроме `terminal`), и это подтверждено мутацией (см. §3).
**2. `accepts_labels` НЕ заполнял** — оставил fail-closed, это ратифицированное верное состояние. Комментарий в `configs/models.yaml` переписан под решённый вокабуляр и под то, ЧЕГО не хватает: значения — это ФАКТЫ ToS/AUP, они (а) берутся только по первоисточникам (правило двух направлений), (б) даются **пер-лейблу**, а не пер-провайдеру целиком — `violence` и `sexually-explicit` у одного вендора разные ответы (ровно на этом стоит D14 п.1), (в) фиксируются полигоном в `experiments/00-provider-quirks.md`, не бэкенд-сессией. Ни одного ToS-утверждения от себя в репо не внёс — по памяти нельзя, а чужую зону занимать не стал.
**3. Фикс-лист:** коммент `Stage.ReasoningMaxTokens` — вторая половина («ignored … reasoning-off») была неверна после ратифицированной формы 2. Переписан: буфер обязателен для ЛЮБОЙ стадии, чья резолвнутая модель на аддитивном провайдере, **включая `reasoning:"off"`** (off гасит thinking только там, где у модели есть выключатель, поэтому исключение было тихим выходом); подавленный вызов лишь пере-резервирует — потолок ужимается, слепым не становится.
**Данные (шиппинг):** `configs/pipeline-c1.yaml` — закомментированный пример реестра дополнен формой `allow` и таблицей «что требует каждое действие»; значения в примере СИНТЕТИЧЕСКИЕ, вокабуляр в шиппинг не прибит.
## §2. Тесты (+4, все обязательные из дельты и сверх)
- `TestAllowActionAssertsWithoutRouting`**позитивный** (книга с assert-only лейблом грузится без цепочки и без бюджета, каждая стадия сохраняет свой конфиговый маршрут) **и негативный** (неспособный эндпойнт → громко; инвариант — единственное, что делает эта политика).
- `TestAllowActionRejectsRoutingKnobs``chain` при `allow` и `label_models` для allow-лейбла отвергаются, а не игнорируются: автор, написавший их, имел в виду то, чего движок не сделает.
- `TestRatifiedLabelPairIsDataOnly` — форма ратифицированного ответа владельца: ДВА лейбла, один только ассертит, второй пере-маршрутизирует; инвариант применяется к ОБЪЕДИНЕНИЮ (модель, принимающая лишь один из двух, отвергается по имени). Значения синтетические — доказывается ФОРМА, поэтому любой вокабуляр ляжет данными.
- `TestAssertOnlyLabelRunsWithoutMovingAnything` — сквозняк через настоящий драйвер: allow-лейблованный прогон **байт-в-байт совпадает** с нелейблованным (тела запросов сравнены поштучно, стоимость равна, оба волновых снапшота НЕ двинулись), и та же книга без capability отказывается запускаться. Это прямое доказательство, что ассерт не имеет wire-эффекта и не может ничего пере-оплатить.
## §3. Ревью исполнением (мутации — каждая новая ветка обязана краснеть)
| мутация | красные тесты |
|---|---|
| инвариант перестаёт применяться к не-route действию (`allow` становится декоративным) | `TestAllowActionAssertsWithoutRouting` · `TestRatifiedLabelPairIsDataOnly` · `TestAssertOnlyLabelRunsWithoutMovingAnything` |
| `chain` при `allow` молча игнорируется | `TestAllowActionRejectsRoutingKnobs` |
| `label_models` для не-route лейбла молча игнорируется | `TestAllowActionRejectsRoutingKnobs` |
Две правки фикстур были неизбежны и отражают НОВУЮ (корректную) семантику, объявляю их явно: (а) кейс `terminal` теперь снимает и `label_models` (они стали громкой ошибкой при не-route действии — что само проверяется тестом выше); (б) ожидаемая подстрока сообщения об неизвестном действии стала `route|allow|terminal`.
## §4. Манифест «заявление = команда»
| # | Заявление | Команда | Результат |
|---|---|---|---|
| D1 | сборка/vet чисты, формат чист | `go build ./... && go vet ./...` · `gofmt -l internal/ cmd/` | без вывода (кроме `internal/llm/llm.go` — состояние HEAD, не моё) |
| D2 | вся сьюта зелёная под `-race` | `go test -race -count=1 ./...` | 12 пакетов `ok`, 0 FAIL |
| D3 | **golden байт-идентичен, пере-капчера нет** | `sha256sum internal/pipeline/testdata/golden/capture.golden` · `git status --porcelain backend/internal/pipeline/testdata/` | `f7641993e37f7ec619dfc5d2914a342d796a7cecd92080e33704f3974f0020f8` · пусто |
| D4 | **парити EXACT** | `TM_MINER_PARITY=1 go test ./internal/miner/ -run Parity -count=1 -v` | `PARITY: n=13618 catastrophe{方源:0 蛊:1 蛊师:2 古月:22} recall@proposed=0.9655 (56/58 GT)` |
| D5 | ценз ИСПОЛНЕНИЕМ: 451 → **455** (+4, 0) | `grep -rn "^func Test" backend \| wc -l` · `git grep -c "^func Test" HEAD -- 'backend/**/*_test.go'` (сумма) · `diff` списков имён | 455 / 451; добавлены ровно 4 названных выше |
| D6 | каждая новая ветка краснеет от мутации | копия дерева в `/tmp`, 3 мутации, `go test ./internal/config/ ./internal/pipeline/` | таблица §3 |
| D7 | вокабуляр в шиппинг НЕ прибит, ToS-факты не выдуманы | `grep -rnE "^\s*(accepts_labels\|content_policy)" backend/configs/` | ни одной раскомментированной строки |
| D8 | движок не знает ни одного значения лейбла | `go test ./internal/config/ -run TestNoPrivilegedLabelValue` | `ok` (книга с «adult» против реестра синтетических лейблов отвергается как любая другая) |
| D9 | сессия не коммитила | `git status --porcelain \| wc -l` | 6 записей, HEAD не двигался |
## §5. Открытые петли (после дельты)
1. **`accepts_labels` — ждут ФАКТОВ по первоисточникам, пер-провайдер И ПЕР-ЛЕЙБЛ** (`violence``sexually-explicit` у одного вендора). Зона полигона (`experiments/00-provider-quirks.md`). До фактов любая лейблованная книга падает громко, называя модель, провайдера и недостающий лейбл — верное состояние.
2. **Q2 (редактор под лейблом)** — отложен с записью при приёмке; связывает только explicit-книги, которых нет.
3. **D22.7** паком не снят: пер-чанковый L3-скрин остаётся предусловием первой erotica-книги в проде; `action: terminal` — book-level отказ входа по объявленным данным, механизм слабее, значение под него не заведено.
4. Хвосты общности (Detail-строки чекеров · множитель ×2) — «отложено-с-записью», причина в отчёте фазы 2 §3.3 (двинули бы снапшот стендовых книг).
5. `--accept-rebill` (D20.2-Q2) — по-прежнему отсутствует в коде; петля не пака-17.