Merge pull request 'ci(545): require a **Signals:** line on decision records' (#547) from fix/545-signals-required into main
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m35s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 20m50s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 14m23s

This commit was merged in pull request #547.
This commit is contained in:
2026-07-21 18:58:03 +00:00
3 changed files with 28 additions and 5 deletions
+8 -4
View File
@@ -16,7 +16,10 @@ A migrated record is an H2 whose first non-blank content line is a metadata bloc
(Blockquoted here so the illustrative `key`/heading don't parse as a real record; the `## 2026-07-17
— No persistent compiler servers …` entry below is a live example.) Five metadata fields: `key`
(dotted, e.g. `ci.runner-placement`), `status`, `since`, `supersedes`, `superseded-by`. `status` is
(dotted, e.g. `ci.runner-placement`), `status`, `since`, `supersedes`, `superseded-by`. The
**`Signals:`** line is also required (validator-enforced, ersatztv#545): it is the keywords/paths
MemPalace's recall matches on, so a record without it ingests with weak recall metadata and
under-surfaces — pack it with synonyms, symbol names, `paths:`, and issue refs. `status` is
one of:
- **`active`** — the current, authoritative record for its `key`. Exactly one active record per key.
- **`superseded`** — reversed by a newer record; relocated to `docs/decisions/archive/` with
@@ -35,9 +38,10 @@ in on both records. Never silently rewrite a record's rationale prose in place.
(this file + the topic files, excluding the archive) via `scripts/build_decisions_catalog.py` — run
it after any status change; CI's `decisions lifecycle` job fails on drift (`--check`).
**Enforcement**: `scripts/decisions_validate.py` checks metadata well-formedness, one-active-record-
per-key, reciprocal links, that no record vanishes from the active set without an archive copy, that
the active catalog is in sync, and an aggregate active-corpus line budget (replaces the old 1800-line
**Enforcement**: `scripts/decisions_validate.py` checks metadata well-formedness (including a required
`Signals:` line), one-active-record-per-key, reciprocal links, that no record vanishes from the active
set without an archive copy, that the active catalog is in sync, and an aggregate active-corpus line
budget (replaces the old 1800-line
floor on this single file). The Husky `pre-commit` hook runs the structural checks over the working
tree; the CI `decisions lifecycle` job additionally runs the body-diff check with `--base`/`--head`.
+5 -1
View File
@@ -18,7 +18,11 @@ from pathlib import Path
import scripts.decisions_lib as dl # noqa: E402 (run with PYTHONPATH=. or as module)
SKIP_HEADINGS = {"Index", "Active catalog", "Contents"}
REQUIRED_META = ("key", "status", "since", "supersedes", "superseded_by")
# `signals` is required alongside the lifecycle fields: the `**Signals:**` line (plus `key:`) is what
# MemPalace's keyword recall matches on when surfacing a decision, so a record without it ingests with
# weak recall metadata and produces confident false-negatives for the "MemPalace to find, file to
# confirm" workflow (ersatztv#545).
REQUIRED_META = ("key", "status", "since", "supersedes", "superseded_by", "signals")
EDIT_TOKEN = "[decisions-edit]" # noqa: S105 (a commit-message marker, not a credential)
+15
View File
@@ -15,6 +15,7 @@ def _rec(**kw: Any) -> dl.Record:
since="2026-01-01",
supersedes="none",
superseded_by="none",
signals="concept · paths: a/b.py · issues: #1",
)
base.update(kw)
return dl.Record(**base)
@@ -131,6 +132,20 @@ def test_missing_since_fails():
assert any("missing required metadata since" in e for e in _v([rec]))
def test_missing_signals_fails():
# `**Signals:**` is required (ersatztv#545): it is what MemPalace's keyword recall matches on, so
# a record without it ingests with weak metadata and under-surfaces ("no convention exists").
assert any("missing required metadata signals" in e for e in _v([_rec(key="a.b", signals=None)]))
def test_empty_signals_fails():
assert any("missing required metadata signals" in e for e in _v([_rec(key="a.b", signals="")]))
def test_signals_present_passes():
assert not any("signals" in e for e in _v([_rec(key="a.b", signals="concept · paths: x.py · issues: #1")]))
def test_demoted_heading_fails():
assert any(
"demoted to legacy-unmigrated" in e for e in _v([_rec(key="a.b")], demoted=["2026-01-01 — Some decision (#1)"])