From 0ef053a209dc5c5ce4cdf7aba3944923c4461842 Mon Sep 17 00:00:00 2001 From: Timothy Date: Tue, 21 Jul 2026 20:19:10 +0200 Subject: [PATCH] ci(545): require a **Signals:** line on decision records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The decisions-guard validator enforced lifecycle metadata (key/status/since/ supersedes/superseded-by + reciprocal links) but not the **Signals:** line — which is exactly what MemPalace's keyword recall matches on. A record without it ingests with weak recall metadata and produces confident false-negatives for the "MemPalace to find, file to confirm" retrieval workflow. Add "signals" to REQUIRED_META so a migrated record with a missing or empty **Signals:** line fails the same way a missing key does. All 114 active + 2 archive records already carry a Signals line, so this is non-breaking on the current corpus. Archive records are intentionally out of scope (recall targets the active corpus). - scripts/decisions_validate.py: signals in REQUIRED_META (+ rationale comment) - scripts/tests/test_decisions_validate.py: _rec() default + missing/empty/present cases - docs/decisions.md: header + Enforcement note the requirement and why fixes #545 Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/decisions.md | 12 ++++++++---- scripts/decisions_validate.py | 6 +++++- scripts/tests/test_decisions_validate.py | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index d601e60b1..7c9863bd0 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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`. diff --git a/scripts/decisions_validate.py b/scripts/decisions_validate.py index 6699dbbd8..4bc22b59f 100644 --- a/scripts/decisions_validate.py +++ b/scripts/decisions_validate.py @@ -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) diff --git a/scripts/tests/test_decisions_validate.py b/scripts/tests/test_decisions_validate.py index 0ffc514ae..b9a9b7998 100644 --- a/scripts/tests/test_decisions_validate.py +++ b/scripts/tests/test_decisions_validate.py @@ -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)"]) -- 2.47.3