Pin every deciding field of the signing sheet against the stop row itself, and make the arm gate assert its own exemption is unused
This commit is contained in:
parent
3f0cd57eb5
commit
881dad4770
7 changed files with 278 additions and 29 deletions
|
|
@ -63,8 +63,11 @@ make mutations # мутационный гейт: подмножество
|
|||
# невалидный каталог · пустое battery-подмножество · неизвестный `-id` · и гниль якорей —
|
||||
# она проверяется статически по ВСЕМУ каталогу, поэтому приходит и из записи вне
|
||||
# подмножества. Полный перечень — ветки, ведущие к ненулевому коду, в cmd/tmmutate/main.go
|
||||
# НЕ входит в battery намеренно: стоит 87–104 % времени батареи (замер на девяти
|
||||
# прогонах) и идёт БЕЗ -race, то есть доказывает меньше, чем цель test при той же цене.
|
||||
# НЕ входит в battery намеренно: стоит примерно столько же времени, сколько сама
|
||||
# батарея, и идёт БЕЗ -race — то есть доказывает меньше, чем цель test, за ту же цену.
|
||||
# Точнее сказать нельзя, и это не небрежность: замеры на одной машине разъезжаются от
|
||||
# фоновой нагрузки шире, чем отличаются друг от друга обе цели, а состав подмножества
|
||||
# с тех пор ещё и вырос.
|
||||
# Состав подмножества живёт в каталоге (поле battery), не здесь
|
||||
make lint # golangci-lint 2.12.2 (версия пиновата в Makefile; конфиг — .golangci.yml)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2816,5 +2816,70 @@
|
|||
"replace": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "BANKSTOP-the-sheet-carries-the-rows-own-frequency",
|
||||
"why": "the frequency beside a proposal is what an owner weighs the term by, and it is the row's own count or it is nothing. Before 08.09 the parity assertion checked Freq only for `!= 0`, so a tenfold rescale in the fold published 120 occurrences for a term seen 12 times and left the package green",
|
||||
"package": "./internal/pipeline/",
|
||||
"battery": true,
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/bankexport.go",
|
||||
"find": "\t\t\tFreq: row.Freq, Spread: row.Spread, Conf: row.Conf, Invented: row.Invented,",
|
||||
"replace": "\t\t\tFreq: row.Freq * 10, Spread: row.Spread, Conf: row.Conf, Invented: row.Invented,"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "BANKSTOP-kind-and-channel-are-not-exchangeable",
|
||||
"why": "the fold RENAMES both (Type->Kind, Origin->Channel) and both are strings, so an exchange compiles and ships a sheet whose every row says its kind is `banknote` and its channel is `name`. Renamed neighbours of one type are the pair a hand-written comparison forgets",
|
||||
"package": "./internal/pipeline/",
|
||||
"battery": true,
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/bankexport.go",
|
||||
"find": "\t\t\tSrc: row.Src, Dst: row.Dst, Kind: row.Type, Channel: row.Origin,",
|
||||
"replace": "\t\t\tSrc: row.Src, Dst: row.Dst, Kind: row.Origin, Channel: row.Type,"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "BANKSTOP-the-two-lists-answer-different-questions",
|
||||
"why": "`contradicts` is what THIS RUN breaks, `bank_holds` is what the book already calls it — separate fields because they are separate decisions (mining.go says so where it fills them). Both are []string, so an exchange compiles; it is invisible against any fixture where both are empty, which is what the stop's fixture gave until its seed carried an unsigned row",
|
||||
"package": "./internal/pipeline/",
|
||||
"battery": true,
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/bankexport.go",
|
||||
"find": "\t\t\tContradicts: row.Contradicts, BankHolds: row.BankHolds,",
|
||||
"replace": "\t\t\tContradicts: row.BankHolds, BankHolds: row.Contradicts,"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "BANKSTOP-an-invented-rendering-is-published-as-invented",
|
||||
"why": "`invented` says no draft proposed this rendering — the class the type's own comment calls the one to read first. Forced false it is simply absent from the sidecar (omitempty), and the signing screen loses the flag that tells an owner to look",
|
||||
"package": "./internal/pipeline/",
|
||||
"battery": true,
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/pipeline/bankexport.go",
|
||||
"find": "\t\t\tFreq: row.Freq, Spread: row.Spread, Conf: row.Conf, Invented: row.Invented,",
|
||||
"replace": "\t\t\tFreq: row.Freq, Spread: row.Spread, Conf: row.Conf, Invented: false,"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ARM-the-one-exemption-is-asserted-unused-not-merely-logged",
|
||||
"why": "reasoning_max_tokens leaves the arm comparison only where the loader forces the value (pipeline.go:771, additive billing). Nothing runs an additive editor today, so the exemption never fires and its correctness is never exercised; widening the predicate makes it fire against every config, and before 08.09 the only trace of that would have been a log line on a green run",
|
||||
"package": "./internal/config/",
|
||||
"battery": true,
|
||||
"edits": [
|
||||
{
|
||||
"file": "internal/config/armparity_test.go",
|
||||
"find": "\treturn m.providerReasoning(model) == \"additive\"",
|
||||
"replace": "\treturn m.providerReasoning(model) != \"\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -66,8 +66,20 @@ func TestAnArmIsTheProductionConfigWithADifferentEditor(t *testing.T) {
|
|||
// reasoning_max_tokens is exempt only where the loader forces it — today that is one model of the
|
||||
// nine in models.yaml (grok-4.3, the only `additive`), and no arm runs it.
|
||||
exempt := "model, resolved model, escalate_to, few_shot"
|
||||
if m.providerReasoning(editorOf(t, arm).Model) == "additive" {
|
||||
exempt += ", reasoning_max_tokens (additive billing forces it; nothing compares its VALUE)"
|
||||
// ⛔ AND THE ONE EXEMPTION THAT IS CONDITIONAL IS ASSERTED TO BE UNUSED, not merely logged.
|
||||
// reasoning_max_tokens is dropped from the comparison only where the loader FORCES a value —
|
||||
// additive billing — because two different models then carry two different forced values and
|
||||
// comparing them would fail for a reason that is not drift. Today no editor on either side is
|
||||
// additive (one model of the nine in models.yaml is, and no config runs it), so the exemption
|
||||
// never fires and its correctness is never exercised. The day an editor moves there it starts
|
||||
// firing silently, and the only trace would be the log line above — which nobody reads on a
|
||||
// green run. So it fails instead: the move is legitimate, the decision it needs is not
|
||||
// automatic, and a gate that quietly stops comparing a key is the defect this file exists for.
|
||||
for side, p := range map[string]*Pipeline{"production": base, filepath.Base(file): arm} {
|
||||
if ed := editorOf(t, p); forcesReasoningBuffer(m, ed.Model) {
|
||||
t.Fatalf("%s now runs editor %q, whose provider bills reasoning additively, so the loader forces reasoning_max_tokens and this gate stops comparing its VALUE. Decide deliberately: either compare the forced values (they are a function of the model, so equal ones mean nothing) or drop the key with a reason written here",
|
||||
side, ed.Model)
|
||||
}
|
||||
}
|
||||
t.Logf("%s: editor %q; exempt from the comparison: %s", filepath.Base(file), editor, exempt)
|
||||
for _, d := range pipelineFieldDiffs(withoutEditorModel(t, m, base), withoutEditorModel(t, m, arm)) {
|
||||
|
|
@ -77,6 +89,17 @@ func TestAnArmIsTheProductionConfigWithADifferentEditor(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// forcesReasoningBuffer is the ONE reading of the loader's rule, used both by the exemption and by the
|
||||
// guard that asserts the exemption is unused. Two copies of one predicate drift, and the drift is silent
|
||||
// in exactly the direction that matters: an exemption firing while the guard says it does not.
|
||||
// It mirrors pipeline.go:771 — the loader refuses a stage resolving to an additive-billing provider with
|
||||
// no reasoning_max_tokens, so there the value is forced and is a function of the model, not of the config.
|
||||
// `additive_total` is deliberately NOT included: the loader does not force the key there either, and a
|
||||
// predicate broader than the rule it reads would exempt a key that nothing forces.
|
||||
func forcesReasoningBuffer(m *Models, model string) bool {
|
||||
return m.providerReasoning(model) == "additive"
|
||||
}
|
||||
|
||||
// armNames renders the discovered files for a log line.
|
||||
func armNames(paths []string) []string {
|
||||
out := make([]string, 0, len(paths))
|
||||
|
|
@ -124,7 +147,7 @@ func withoutEditorModel(t *testing.T, m *Models, p *Pipeline) Pipeline {
|
|||
continue
|
||||
}
|
||||
// Read the model from p: out's copy is blanked two lines below.
|
||||
if m.providerReasoning(p.Stages[i].Model) == "additive" {
|
||||
if forcesReasoningBuffer(m, p.Stages[i].Model) {
|
||||
out.Stages[i].ReasoningMaxTokens = 0
|
||||
}
|
||||
out.Stages[i].Model, out.Stages[i].ResolvedModel = "", ""
|
||||
|
|
|
|||
94
backend/internal/pipeline/bankexport_test.go
Normal file
94
backend/internal/pipeline/bankexport_test.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package pipeline
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// projectBankProposals is a pure fold, and the property that matters about it — no deciding field is
|
||||
// dropped, swapped with its neighbour or rescaled on the way to the sidecar — is a property of the fold
|
||||
// alone. It gets its own test here rather than riding on the stop's integration fixture, because that
|
||||
// fixture's values are whatever one run happens to produce: 0 for the counters a small corpus never
|
||||
// raises, empty for both string lists, false for a rendering the drafts did propose. Compared against a
|
||||
// row of that shape, a swap reads as `0 != 0` and a tenfold rescale as `!= 0` — the assertion passes
|
||||
// while the projection lies. Measured on 08.09: four mutations planted at once (Freq×10, Invented forced
|
||||
// false, Contradicts↔BankHolds swapped) left the whole package green in 16.8 s.
|
||||
//
|
||||
// So the row below gives EVERY field a value that is distinguishable from every other field's: the two
|
||||
// string lists differ in length AND in content, Freq/Spread/Conf are three different non-zero numbers,
|
||||
// Kind and Channel are two different strings, and the rendering is one no draft proposed.
|
||||
func TestEveryDecidingFieldSurvivesTheProjection(t *testing.T) {
|
||||
row := BankStopRow{
|
||||
Src: "方源",
|
||||
Dst: "Фан-Юань",
|
||||
Type: "name",
|
||||
Origin: "banknote",
|
||||
Freq: 37,
|
||||
Spread: 4,
|
||||
Conf: 61,
|
||||
Invented: true,
|
||||
Contradicts: []string{
|
||||
`this run also renders 方 as "Фан"`,
|
||||
},
|
||||
BankHolds: []string{
|
||||
`unsigned draft "方源"→"Странник" [ch 1..20]`,
|
||||
`approved "方源"→"Фан Юань" [ch 21..40]`,
|
||||
},
|
||||
Variants: []BankStopVariant{
|
||||
{Dst: "Фан Юань", Chunks: 9},
|
||||
{Dst: "Фан", Chunks: 2, Via: "方"},
|
||||
},
|
||||
// Fields the sidecar deliberately does NOT carry, set anyway: a projection that started copying
|
||||
// them into the wrong slot would move a value that is supposed to stay behind.
|
||||
Contexts: []string{"方源来到青茅山"},
|
||||
Evidence: []string{"ch 3"},
|
||||
Conventions: 2,
|
||||
Signals: []string{"majority"},
|
||||
}
|
||||
|
||||
got := projectBankProposals([]BankStopRow{row})
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("one row projected to %d proposal(s)", len(got))
|
||||
}
|
||||
want := BankExportProposal{
|
||||
Src: "方源", Dst: "Фан-Юань", Kind: "name", Channel: "banknote",
|
||||
Freq: 37, Spread: 4, Conf: 61, Invented: true,
|
||||
Contradicts: []string{`this run also renders 方 as "Фан"`},
|
||||
BankHolds: []string{
|
||||
`unsigned draft "方源"→"Странник" [ch 1..20]`,
|
||||
`approved "方源"→"Фан Юань" [ch 21..40]`,
|
||||
},
|
||||
Variants: []string{"Фан Юань ×9", "Фан ×2 (proposed for 方)"},
|
||||
}
|
||||
if !reflect.DeepEqual(got[0], want) {
|
||||
t.Errorf("the projection is not the row it projects:\n got %+v\n want %+v", got[0], want)
|
||||
}
|
||||
|
||||
// AND: no field of the proposal is left at its zero value. The comparison above pins the fields that
|
||||
// exist today; this pins the ones added tomorrow — a field appended to BankExportProposal and never
|
||||
// filled by the fold ships as an absent key in the sidecar, and every assertion written against
|
||||
// today's fields stays green while the signing screen loses a column.
|
||||
//
|
||||
// ⚠ Its limit, named: this catches a field the PROPOSAL grew and the fold ignored. A field BankStopRow
|
||||
// grows that ought to be projected and is not cannot be caught mechanically — whether a new fact is
|
||||
// one an owner signs on is a judgement, and it belongs to whoever adds the field.
|
||||
v := reflect.ValueOf(got[0])
|
||||
for i := range v.NumField() {
|
||||
if v.Field(i).IsZero() {
|
||||
t.Errorf("field %s is zero in the projection of a row where every projected field was set — the fold does not carry it",
|
||||
v.Type().Field(i).Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An empty table must project to nil rather than to an empty slice: `omitempty` then keeps the key out of
|
||||
// the sidecar entirely, and a reader that finds `proposed` present with zero rows cannot tell "the stop
|
||||
// presented nothing" from "this boundary is not a stop".
|
||||
func TestATableWithNoRowsProjectsToNoSection(t *testing.T) {
|
||||
if got := projectBankProposals(nil); got != nil {
|
||||
t.Errorf("no rows projected to %#v, want nil so the key is omitted entirely", got)
|
||||
}
|
||||
if got := projectBankProposals([]BankStopRow{}); got != nil {
|
||||
t.Errorf("an empty table projected to %#v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -928,14 +929,23 @@ func TestBankStopPublishesItsProposalsInTheReadOut(t *testing.T) {
|
|||
rec := &reqRec{}
|
||||
srv := newJSONProvider(rec, func(body string) (string, string) {
|
||||
if isTerminologyBody(body) {
|
||||
return "方源\tФан Юань", "stop"
|
||||
// NOT the rendering the drafts proposed (that is «Фан Юань», in bankBlockForMining): the role
|
||||
// reads the whole book and the drafts read fragments, so a consolidation of its own is the
|
||||
// ordinary case — and it makes Invented TRUE on the published row rather than false-by-luck.
|
||||
return "方源\tФан-Юань", "stop"
|
||||
}
|
||||
return "Фан Юань пришёл." + "\n" + bankBlockForMining, "stop"
|
||||
})
|
||||
defer srv.Close()
|
||||
// A seeded row this corpus never yields, so the BANK half of the document is non-empty and "the stop
|
||||
// left it alone" is a real comparison instead of "" == "".
|
||||
const seed = "terms:\n - src: 蛊师\n dst: гу-мастер\n status: approved\n decl: { invariant: true, forms: [\"гу-мастер\"] }\n"
|
||||
// TWO seeded rows, and neither is decoration. 蛊师 is one this corpus never yields, so the BANK half of
|
||||
// the document is non-empty and "the stop left it alone" is a real comparison instead of "" == "".
|
||||
// 方源 is an UNSIGNED row for a surface the run consolidates differently, which is what fills BankHolds
|
||||
// on the published proposal — without it that list is empty, and so is Contradicts, and a projection
|
||||
// that swapped the two would compare as "" against "" and pass. until_ch keeps the seed row off the
|
||||
// proposal's unique key, so this is a disagreement rather than a replacement (the idiom
|
||||
// TestConsolidationAgainstTheBankIsReportedAtTheStop established).
|
||||
const seed = "terms:\n - src: 蛊师\n dst: гу-мастер\n status: approved\n decl: { invariant: true, forms: [\"гу-мастер\"] }\n" +
|
||||
" - { src: 方源, dst: Странник, status: draft, until_ch: 20 }\n"
|
||||
r := newVerifyRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, glossarySeed: seed}))
|
||||
defer r.Close()
|
||||
stop := runToSignatureStop(t, r)
|
||||
|
|
@ -950,6 +960,15 @@ func TestBankStopPublishesItsProposalsInTheReadOut(t *testing.T) {
|
|||
if len(exp.Proposed) != len(stop.Rows) {
|
||||
t.Fatalf("the read-out carries %d proposals, the stop presented %d — the screen and the table would disagree", len(exp.Proposed), len(stop.Rows))
|
||||
}
|
||||
// ⛔ The section is the WHOLE fold of the table it came from — and this comparison catches exactly one
|
||||
// class, deliberately: what the fold produced is what survived serialization and reached the file, in
|
||||
// this order, at this boundary. It CANNOT see a defect in the fold itself, because a broken fold moves
|
||||
// both sides of it equally; measured on 08.09, Freq×10 and a Kind↔Channel swap both survive this line.
|
||||
// The fold's own correctness lives in TestEveryDecidingFieldSurvivesTheProjection, on a row where every
|
||||
// field differs from every other, and the value anchors below cross-check it from the run's side.
|
||||
if want := projectBankProposals(stop.Rows); !reflect.DeepEqual(exp.Proposed, want) {
|
||||
t.Fatalf("the published section is not the fold of the table it came from:\n in the file %+v\n from the rows %+v", exp.Proposed, want)
|
||||
}
|
||||
var fy *BankExportProposal
|
||||
for i := range exp.Proposed {
|
||||
if exp.Proposed[i].Src == "方源" {
|
||||
|
|
@ -959,14 +978,28 @@ func TestBankStopPublishesItsProposalsInTheReadOut(t *testing.T) {
|
|||
if fy == nil {
|
||||
t.Fatalf("方源 is missing from the published proposals: %+v", exp.Proposed)
|
||||
}
|
||||
if fy.Dst != "Фан Юань" || fy.Freq == 0 || len(fy.Variants) == 0 {
|
||||
if fy.Dst != "Фан-Юань" || fy.Freq == 0 || len(fy.Variants) == 0 {
|
||||
t.Fatalf("proposal = %+v, want the consolidated rendering, a frequency and the drafts' variants", *fy)
|
||||
}
|
||||
// ⛔ AND EVERY DECIDING FIELD, compared against the stop row it is projected from. The section exists so
|
||||
// the signing screen can DECIDE, and the type's own comments say which fields that takes: the channel
|
||||
// that found it, the disagreement count, the role's stated confidence, whether the rendering is one no
|
||||
// draft proposed, and — as two separate answers — what this run contradicts and what the bank already
|
||||
// holds. Dropping any of them leaves a document that still parses and can no longer be signed from.
|
||||
// THE FIXTURE IS NOT DEGENERATE, asserted rather than assumed. Every one of these was false before
|
||||
// 08.09, and with all three false the comparison above degenerates: `false != false`, `0 != 0`, `0 != 0`
|
||||
// pass whatever the fold did. Measured then: Freq×10, Invented forced false and the two lists swapped —
|
||||
// all four planted at once — left the package green.
|
||||
if !fy.Invented {
|
||||
t.Fatalf("premise broken: the published rendering %q is one the drafts proposed, so `invented` is false and no assertion about it means anything: %+v", fy.Dst, *fy)
|
||||
}
|
||||
if len(fy.BankHolds) == 0 {
|
||||
t.Fatalf("premise broken: the seeded row for %q did not reach the proposal as a bank hold, so bank_holds is empty and indistinguishable from contradicts: %+v", fy.Src, *fy)
|
||||
}
|
||||
if len(fy.Contradicts) != 0 {
|
||||
t.Fatalf("premise broken the other way: this case is a disagreement with the BANK, not with the run itself; a non-empty contradicts means the fixture stopped being the one this test describes: %+v", *fy)
|
||||
}
|
||||
|
||||
// VALUE ANCHORS against the stop row itself — the fold is not its own witness here. Every one is a
|
||||
// field the fold RENAMES or could exchange with a neighbour of the same type, so a rescale or a swap
|
||||
// has somewhere to show. Compared by value: the earlier form of this block compared the two string
|
||||
// lists by LENGTH, and against a row whose lists were both empty that read as 0 != 0 and passed
|
||||
// whatever the fold did.
|
||||
var src *BankStopRow
|
||||
for i := range stop.Rows {
|
||||
if stop.Rows[i].Src == fy.Src {
|
||||
|
|
@ -974,23 +1007,36 @@ func TestBankStopPublishesItsProposalsInTheReadOut(t *testing.T) {
|
|||
}
|
||||
}
|
||||
if src == nil {
|
||||
t.Fatalf("no stop row for %q to compare the projection against", fy.Src)
|
||||
t.Fatalf("no stop row for %q to anchor the projection against", fy.Src)
|
||||
}
|
||||
if fy.Kind != src.Type || fy.Channel != src.Origin || fy.Spread != src.Spread ||
|
||||
fy.Conf != src.Conf || fy.Invented != src.Invented ||
|
||||
len(fy.Contradicts) != len(src.Contradicts) || len(fy.BankHolds) != len(src.BankHolds) ||
|
||||
len(fy.Variants) != len(src.Variants) {
|
||||
t.Fatalf("the projection lost deciding fields:\n published %+v\n stop row kind=%q channel=%q spread=%d conf=%d invented=%v contradicts=%d bank_holds=%d variants=%d",
|
||||
*fy, src.Type, src.Origin, src.Spread, src.Conf, src.Invented, len(src.Contradicts), len(src.BankHolds), len(src.Variants))
|
||||
if fy.Kind != src.Type || fy.Channel != src.Origin {
|
||||
t.Errorf("the sheet names kind=%q channel=%q; the row says type=%q origin=%q — these two are renamed by the fold and are exactly what an exchange would hide",
|
||||
fy.Kind, fy.Channel, src.Type, src.Origin)
|
||||
}
|
||||
|
||||
if exp.Total != 1 || exp.Signed != 1 {
|
||||
t.Fatalf("the bank half moved: total=%d signed=%d, want the single seeded row — a proposal must not be banked by being published", exp.Total, exp.Signed)
|
||||
if fy.Freq != src.Freq || fy.Spread != src.Spread || fy.Conf != src.Conf {
|
||||
t.Errorf("the sheet counts freq=%d spread=%d conf=%d; the row counts %d/%d/%d — a frequency that is not the row's is a number the owner signs against nothing",
|
||||
fy.Freq, fy.Spread, fy.Conf, src.Freq, src.Spread, src.Conf)
|
||||
}
|
||||
if exp.Total != 2 || exp.Signed != 1 {
|
||||
t.Fatalf("the bank half moved: total=%d signed=%d, want the two seeded rows with one signature — a proposal must not be banked by being published", exp.Total, exp.Signed)
|
||||
}
|
||||
// The surface itself IS in the bank — that is the seeded draft this case is built on. What must not be
|
||||
// there is the proposal's own RENDERING: publishing a row for signature must not sign it. So the two
|
||||
// are told apart by dst, not by src, and the seeded row is asserted to have stayed exactly as seeded.
|
||||
var seeded bool
|
||||
for _, term := range exp.Terms {
|
||||
if term.Src == "方源" {
|
||||
if term.Src != fy.Src {
|
||||
continue
|
||||
}
|
||||
if term.Dst == fy.Dst {
|
||||
t.Fatalf("the proposal was written INTO the bank: %+v", term)
|
||||
}
|
||||
if term.Dst == "Странник" {
|
||||
seeded = true
|
||||
}
|
||||
}
|
||||
if !seeded {
|
||||
t.Fatalf("premise broken: the seeded draft for %q is gone from the bank, so «publishing did not touch the bank» is being asserted about a row that is not there: %+v", fy.Src, exp.Terms)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import (
|
|||
)
|
||||
|
||||
// snapshot_wave_test.go: WS1 §1в per-wave snapshot mechanics — the folds the wave driver pins every job to.
|
||||
// ⚠ The book-global snapshotID() is TEST-ONLY (no production caller) and is NOT exercised here; its four
|
||||
// callers live in other files and each pins one payload input. The golden pins the two per-wave ids and
|
||||
// ⚠ The book-global snapshotID() is TEST-ONLY (no production caller) and is NOT exercised here; it is
|
||||
// called from four other files, five call sites between them, and each pins one payload input. The golden pins the two per-wave ids and
|
||||
// their payloads, so nothing anywhere should be read as "the book-global fold is covered".
|
||||
func TestSnapshotIDForWave(t *testing.T) {
|
||||
r := newRunner(t, setupProject(t, "http://127.0.0.1:1"))
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@
|
|||
- `make battery` целиком: **2 мин 55 с** (exit 0, `0 issues` линта, 19 `ok` / 0 `FAIL`).
|
||||
- `make mutations` (11 записей): **3 мин 3 с** (exit 0, 11 RED, 0 unexpected, гниль каталога 0).
|
||||
|
||||
⚠ Обе цифры — состав ДО дофикса (11 записей). После дофикса состав 14, и пара пере-снята ОДНИМ прогоном каждая: батарея **3 мин 14 с**, мутационная цель **3 мин 59,7 с** — это **123 %**, то есть ВЫШЕ диапазона 87–104 %, который дали девять до-дофиксных пар. Одна пара диапазона не задаёт, но направление однозначно: три новые записи цель удлинили, и решение «соседней целью» дофикс укрепил — внутри `battery` подмножество удвоило бы её время.
|
||||
⚠ Обе цифры — состав ДО дофикса (11 записей). После дофикса состав 14, и пара пере-снята ОДНИМ прогоном каждая: батарея **3 мин 14 с**, мутационная цель **3 мин 59,7 с** — это **123 %**, то есть ВЫШЕ диапазона 87–104 %, который дали девять до-дофиксных пар. Одна пара диапазона не задаёт, но направление однозначно: три новые записи цель удлинили, и решение «соседней целью» дофикс укрепил — внутри `battery` подмножество удвоило бы её время. ⚠⚠ **ПОПРАВКА 08.09: числу «123 %» веры нет, и я его отзываю.** Парный замер спина-к-спине дал **102 %**, то есть мои 123 % были фоновой нагрузкой машины, а не свойством цели. Верное утверждение — «стоит примерно столько же, сколько батарея»; любая цифра точнее разъезжается от загрузки шире, чем цели отличаются друг от друга. Носитель в `backend/README.md` пере-снят той же формулировкой.
|
||||
|
||||
Довод «внутрь battery» звучит сильно — гейт, который никто не зовёт, воспроизводит дефект строки 313 на шаг выше. Отношение замерено на девяти прогонах и это ДИАПАЗОН, а не одна пара: мутационная цель стоит **87–104 % времени всей батареи** (крайние пары 2:42/3:06 и 3:03/2:55). Брать край как довод нельзя — беру диапазон — то есть внутрь она не «добавилась бы», а почти удвоила бы каждый прогон. Он проигрывает этому и ещё одному факту: **`tmmutate` гоняет пакет БЕЗ `-race`, то есть доказывает МЕНЬШЕ, чем цель `test`, а стоит от неё заметную долю.** Класть внутрь `battery` более слабый по гарантиям прогон ценой такого прироста — плохой размен на цели, которую сессии зовут по десять раз в день. Соседняя цель названа в `Makefile` рядом с `battery`, стоит одной строки в промте следующей смены и не удорожает каждый прогон.
|
||||
⚠ **Это моё решение, и у него есть цена, которую я называю прямо: соседняя цель может остаться незваной, и тогда дефект строки 313 просто переедет на уровень выше.** Если оркестратор считает, что размен другой, — это правка одной строки: `battery: build vet fmt lint test mutations`.
|
||||
|
|
@ -681,6 +681,24 @@
|
|||
|
||||
**Числа, снятые ПОСЛЕ восстановления рецепта, ничем после них дерево не тронуто.** `make mutations` → exit 0, **3m52.6s**, «14 mutation(s) run, 0 unexpected outcome(s)», 14 RED, `anchors swept: 0 of 225 entr(ies) rotten`. Затем `make battery` → exit 0, **3m40.0s**, `0 issues`, `ok` 19 / `FAIL` 0 / «no test files» 4, скипов 4 — те же. Мусора после обоих прогонов: `.skips.log` в дереве **0**, осиротевших `/tmp/tmp.*` **0** (контроль: файлов в корне зоны 17). Записи `ARM-…` в подмножестве это касается прямо — она сажается в `pipeline-arm-glm.yaml`, чью шапку правил F7, и краснеет тем же механизмом, что и до правки.
|
||||
|
||||
#### ВТОРОЙ ДОФИКС ПОСЛЕ ЛЕНДИНГА — живая дыра в починке моей же находки F3 (08.09, к акту `D39.225`)
|
||||
|
||||
Две независимые линзы разными посадками нашли ОДНО: **паритет строки предложения сверял два списка ПО ДЛИНЕ, а фикстура давала обоим пустой список и `Invented: false`.** Три сравнения вырождались в `0 != 0` и `false != false`, а `Freq` проверялся только на `!= 0`. Пере-мерено моей рукой: **четыре мутации разом** — `Freq*10`, `Invented := false`, `Contradicts ↔ BankHolds` — пакет `ok` за 16,8 с. Это `D39.208` п.5, воспроизведённый ВНУТРИ фикса моей же находки, и садится он ровно на пару, чьи комментарии в коде объясняют, что «прогон противоречит сам себе» и «книга уже зовёт это иначе» — РАЗНЫЕ решения.
|
||||
|
||||
**Лечение — разделение работы, а не ещё одно сравнение.** Свёртка `projectBankProposals` — чистая функция, и свойство «ни одно решающее поле не потеряно, не переставлено с соседом и не пере-масштабировано» есть свойство её одной. Она получила собственный тест `TestEveryDecidingFieldSurvivesTheProjection` (`bankexport_test.go`) на строке, где КАЖДОЕ поле отличимо от каждого: два списка расходятся и длиной, и содержимым; `Freq`/`Spread`/`Conf` — три разных ненулевых числа; `Kind` и `Channel` — две разные строки; рендеринг — тот, которого не предлагал ни один черновик. Плюс рефлективный страж: ни одно поле проекции не осталось нулевым — это ловит поле, ДОБАВЛЕННОЕ к предложению завтра и не заполненное свёрткой. Его границу называю: поле, которое отрастил `BankStopRow` и которое СЛЕДОВАЛО бы проецировать, механически не ловится — «решающее ли это» есть суждение, и оно принадлежит тому, кто поле добавляет.
|
||||
|
||||
**Фикстура интеграционного теста пере-сделана невырожденной, и невырожденность УТВЕРЖДАЕТСЯ в нём же.** Сид несёт вторую строку — неподписанный `方源 → Странник` с `until_ch: 20` (идиома `TestConsolidationAgainstTheBankIsReportedAtTheStop`), отчего у опубликованного предложения непустой `bank_holds`; роль терминологии возвращает `Фан-Юань` — рендеринг, которого черновики не предлагали, отчего `invented` истинно не по случайности. Три стража предпосылки падают с текстом «premise broken», если фикстура перестанет быть той, которую тест описывает.
|
||||
|
||||
⛔ **И собственная проверка этой починки нашла в ней дыру — печатаю, потому что иначе отчёт был бы чище работы.** Первая редакция сверяла секцию файла со свёрткой ТОЙ ЖЕ таблицы, то есть свёртку с самой собой: `Freq*10` и своп `Kind↔Channel` эту сверку **пережили** — оба берега двигаются вместе. Добавлены якоря ЗНАЧЕНИЙ против самой стоп-строки (`Kind`/`Channel` — переименованные соседи одного типа; `Freq`/`Spread`/`Conf` — числа). После этого все пять посадок краснеют, и три — точным текстом с числами (`freq=120 … the row counts 12`, `kind="banknote" channel="name"`, `conf=1 … -1`). ⚠ Оставшиеся две ловит СТРАЖ ПРЕДПОСЫЛКИ, а не сравнение: текст говорит «bank_holds пуст и неотличим от contradicts» — это правый вердикт по правой причине, но своп, сохраняющий непустоту обеих сторон, здесь бы уцелел. Он падает на юнит-тесте, где оба списка непусты и различны; разделение названо в комментарии, чтобы следующая смена не приняла интеграционный за сторожа свёртки.
|
||||
|
||||
**Каталог: 225 → 230** (+5, `battery` 14 → 19). Четыре записи на свёртку (`BANKSTOP-the-sheet-carries-the-rows-own-frequency` · `BANKSTOP-kind-and-channel-are-not-exchangeable` · `BANKSTOP-the-two-lists-answer-different-questions` · `BANKSTOP-an-invented-rendering-is-published-as-invented`) и одна на арм-гейт (ниже). Битых якорей после всех правок — 0 при 230 записях.
|
||||
|
||||
**Латентное — прикрыла, а не отдала строкой.** Ничто не утверждало, что редактор боевого конфига НЕ additive-биллинга; переехал бы он туда — изъятие `reasoning_max_tokens` из арм-паритета начало бы срабатывать МОЛЧА, и единственным следом была бы строка лога, которую на зелёном прогоне не читают. Теперь гейт УТВЕРЖДАЕТ, что изъятие не используется, и падает с текстом, называющим решение. Условие изъятия и условие стража сведены в один предикат `forcesReasoningBuffer` — две копии одного правила разъезжаются именно в ту сторону, которая важна. Предъявлено ИСПОЛНЕНИЕМ дважды: (1) боевой редактор переведён на `grok-4.3` + `reasoning_max_tokens` (без ключа загрузчик отказывает РАНЬШЕ гейта — первая посадка краснела не тем механизмом) → страж говорит «production now runs editor "grok-4.3" … Decide deliberately»; (2) предикат расширен до `!= ""` → страж срабатывает на боевом `deepseek-v4-pro`, то есть само условие тоже запинено (запись каталога `ARM-the-one-exemption-is-asserted-unused-not-merely-logged`). ⚠ Граница: `additive_total` в предикат НЕ входит намеренно — загрузчик (`pipeline.go:771`) там ключ тоже не форсирует, и предикат шире правила изымал бы то, чего никто не форсирует.
|
||||
|
||||
**Числа, снятые ПОСЛЕ последней правки (порядок — сперва мутации, затем батарея; дерево между ними не тронуто).** `make mutations` → exit 0, **5m13.7s**, «19 mutation(s) run, 0 unexpected outcome(s)», 19 RED, выживших **0**, `anchors swept: 0 of 230 entr(ies) rotten`. `make battery` → exit 0, **3m39.4s**, `0 issues`, `ok` 19 / `FAIL` 0 / «no test files» 4, скипов 4 — те же четыре. Подмножество выросло 14 → 19, и цель подорожала 3m52 → 5m13: это по-прежнему «примерно столько же, сколько батарея», и это ровно та причина, по которой в README теперь нет процента.
|
||||
|
||||
**Две мелочи.** `snapshot_wave_test.go` говорил «four callers», соседний `snapshot.go` — «five call sites in four test files»: оба верны, единица не названа; теперь единица названа в обоих. `backend/README.md` печатал диапазон цены гейта `87–104 %`, снятый на СТАРОМ составе из 11 записей, и об этом не говорил — пере-снят формулировкой без процента, вместе с отзывом моего же числа «123 %» (см. поправку выше по секции: парный замер спина-к-спине дал 102 %, то есть 123 % были фоновой нагрузкой, а не свойством цели).
|
||||
|
||||
### Пак «ЗАКОН БАНКА — ЧЕСТНО» (06.09, промт `docs/BACKEND_CONSISTENCY_SESSION_PROMPT.md`, вход HEAD `e4097cb`). НЕ КОММИЧУ — ждёт лендинга
|
||||
|
||||
**Итог: закон банка описан тем, что код делает, и найдена дыра ШИРЕ заказанной — стадия схлопывания не сверяла свою выработку со строкой банка на тот же ключ ни при какой подписи.** Закрыто в двух местах (схлопывание + страховка на слиянии), репортом, без смены поведения платного прогона. **ШЕСТЬ кругов самопроверки нашли 43 находки В МОЕЙ ЖЕ работе** (18 + 8 + 5 + 6 + 4 + 2); круг 6 сказал «сошлись» — новых находок в коде пака нет. Шесть находок — ложные утверждения в моих же комментариях, то есть ровно тот класс, который пак чинил; две внесены лечением предыдущего круга; один и тот же шов «писатель ↔ читатель» ловился ТРИ круга подряд, после чего был убран целиком, а не подпёрт четвёртой заплатой. Все разобраны, списки ниже.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue