stacks: the box converts a PostgreSQL major as a guarded-update step (09 6.4 part 10, decisions 35/37/38)
gates / gates (push) Successful in 25s

A step whose ladder entry carries engine_conversion {service, engine, from, to}
converts the database: the old engine alone, the check (owners, roles,
extensions, per-table row counts), pg_dumpall validated by its completion line,
the volume emptied only after the undo copy's marker is validated again, the new
engine alone, the load with ON_ERROR_STOP, the check again + PG_VERSION. Any
failure goes to the existing undo; a restart during converting is undone.
A PostgreSQL major move without the mark is refused before anything moves.
The old datadir's copy is kept until a backup is proven after the conversion.
17 tests, 9 red-proofs (audits/night-2026-09-26/B/).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-25 12:57:03 +02:00
parent 3d49df1e5a
commit 2caae38a71
13 changed files with 1523 additions and 12 deletions
+9 -2
View File
@@ -48,6 +48,10 @@ type LadderEntry struct {
// automatic leg never takes a step that needs a person, and takes a files-may-change step only when
// a fresh WHOLE copy exists. A person's press ignores both — the marks bind the leg only.
Marks LadderMarks `yaml:"marks" json:"marks"`
// EngineConversion (v0.273.0, `09` §6.4 part 10) is the harness's mark that this step converts a
// PostgreSQL major (pgconvert.go). omitempty: an entry without it prints exactly as before, so R-680's
// failed-step records (tied to LadderPrint) are not reset by this field's arrival.
EngineConversion *EngineConversion `yaml:"engine_conversion" json:"engine_conversion,omitempty"`
}
// LadderMarks is the `marks` object of a ladder entry. NeedsPerson is JSON null (nil) or the tester's
@@ -137,6 +141,9 @@ type LadderStep struct {
Meta string
// Why is one operator-English sentence for the log.
Why string
// Entry is the ladder entry this step applies (nil when the installed version matches no entry, or
// the template has no ladder) — its marks, including the engine conversion (v0.273.0).
Entry *LadderEntry
}
// nextLadderStep decides WHICH definition the next press pins, from the catalog template directory
@@ -172,7 +179,7 @@ func nextLadderStep(templateDir string, pinned map[string]string) (LadderStep, e
}
left := len(ladder) - idx
if idx == len(ladder)-1 {
return LadderStep{Index: idx, Left: left, Source: current, Meta: currentMeta,
return LadderStep{Index: idx, Left: left, Source: current, Meta: currentMeta, Entry: &ladder[idx],
Why: fmt.Sprintf("the last step (%d of %d) — the catalog's current definition", idx+1, len(ladder))}, nil
}
src := StepFile(templateDir, ladder[idx].To)
@@ -187,7 +194,7 @@ func nextLadderStep(templateDir string, pinned map[string]string) (LadderStep, e
if _, err := os.Stat(meta); err != nil {
meta = currentMeta // a step written before R-664: the template's own, said in the log
}
return LadderStep{Index: idx, Left: left, Source: src, Meta: meta,
return LadderStep{Index: idx, Left: left, Source: src, Meta: meta, Entry: &ladder[idx],
Why: fmt.Sprintf("step %d of %d: %s → %s, from %s (probe from %s)", idx+1, len(ladder), summarisePin(ladder[idx].From), summarisePin(ladder[idx].To), filepath.Base(src), filepath.Base(meta))}, nil
}