feat(membrane): ao membrane digest filters reason-less placeholder classes so the checklist is actionable (age-7758)

After class-normalization the digest collapsed 22->11 classes, but the top-5 was
STILL all reason-less `pawl-review REFUTED (see evidence)` placeholders (historical,
evidence gone) — the one substantive class (a `[gates]` gate-routing gap finding) sat
buried at rank 6. Injecting placeholders into the pre-mortem checklist = pure noise.

- yieldledger.IsPlaceholderReason: conservative predicate for a non-substantive
  reason — a bare/near-bare token ("r"), the pawl verdict STAMP itself (anchored
  regex on "pawl-review REFUTED/CONFIRMED ..." so digit-less bead ids stripBeadRefs
  leaves intact are still caught), or a reason that normalizes to only disposition
  boilerplate + bead-id fragments. A real defect sentence keeps its content tokens
  and is NEVER flagged (guarded by tests, incl. a reason that mentions the pawl
  mid-sentence).
- ao membrane digest EXCLUDES placeholder classes by default so real-reason classes
  lead the emitted .agents/pre-mortem-checks/catch-digest.md; --include-placeholders
  restores them for corpus auditing, always ranked BELOW every actionable class.
  The checklist reports how many placeholders it filtered (honest, no moat language).

Real-ledger payoff: BEFORE top-5 was 5 placeholders; AFTER the default digest shows
exactly the 1 substantive [gates] class and filters all 10 placeholders — an honest
result that the corpus is still placeholder-dominated and needs real-reason catches
to accrue.
This commit is contained in:
boshu
2026-07-09 08:50:25 -04:00
parent a7b4aae04e
commit 1f346c741a
5 changed files with 342 additions and 33 deletions
+115 -26
View File
@@ -41,8 +41,9 @@ const (
)
var (
membraneDigestTopN int
membraneDigestJSON bool
membraneDigestTopN int
membraneDigestJSON bool
membraneDigestIncludePlaceholders bool
)
var membraneDigestCmd = &cobra.Command{
@@ -61,6 +62,13 @@ writes escape checks into, so the START of the loop front-loads the most-recurri
misses before touching anything. This is the domain-LESS twin of
` + "`ao membrane recall --include-catches`" + ` (a per-domain review-time query).
ACTIONABLE by default: reason-less PLACEHOLDER classes — a bare pawl verdict
("pawl-review REFUTED (see evidence)"), a bare token ("r"), disposition boilerplate
— carry no defect content, so a "watch for: pawl-review REFUTED" line is pure noise.
They are EXCLUDED by default so real-reason classes lead the checklist. Pass
--include-placeholders to restore them (for corpus auditing) — they always rank BELOW
every actionable class.
The file is (re)generated on every run — idempotent, safe to re-run. It is the
AUTO-mined sink and is kept SEPARATE from the human-curated
docs/gate/findings-ledger.md (the Standing Review Dimensions that behavior-first
@@ -76,6 +84,7 @@ func init() {
membraneCmd.AddCommand(membraneDigestCmd)
membraneDigestCmd.Flags().IntVar(&membraneDigestTopN, "top", catchDigestDefaultTopN, "How many top recurring catch classes to include")
membraneDigestCmd.Flags().BoolVar(&membraneDigestJSON, "json", false, "Also print the ranked digest as JSON (the checklist file is written either way)")
membraneDigestCmd.Flags().BoolVar(&membraneDigestIncludePlaceholders, "include-placeholders", false, "Include reason-less placeholder classes (e.g. \"pawl-review REFUTED (see evidence)\") for corpus auditing; excluded by default so the checklist stays actionable")
}
// catchDigestEntry is one ranked recurring catch class in the digest.
@@ -89,15 +98,26 @@ type catchDigestEntry struct {
AffectedPaths []string `json:"affected_paths,omitempty"`
// WatchFor is the deterministic "watch-for-this" imperative (no LLM call).
WatchFor string `json:"watch_for"`
// Placeholder marks a reason-less class (no defect content) — only ever surfaced
// under --include-placeholders, and always ranked below the actionable classes.
Placeholder bool `json:"placeholder,omitempty"`
}
// catchDigest is the whole ranked digest — the JSON shape and the render input.
type catchDigest struct {
GeneratedAt string `json:"generated_at"`
TopN int `json:"top_n"`
TotalClasses int `json:"total_classes"`
TotalHits int `json:"total_hits"`
Entries []catchDigestEntry `json:"entries"`
GeneratedAt string `json:"generated_at"`
TopN int `json:"top_n"`
// TotalClasses is the FULL corpus size (actionable + placeholder), so the digest
// reports how much it summarizes even when placeholders are filtered out.
TotalClasses int `json:"total_classes"`
// ActionableClasses is the count of real-reason (non-placeholder) classes — what a
// planner can actually act on. PlaceholderClasses is the reason-less remainder that
// is filtered by default (age-7758).
ActionableClasses int `json:"actionable_classes"`
PlaceholderClasses int `json:"placeholder_classes"`
IncludePlaceholders bool `json:"include_placeholders"`
TotalHits int `json:"total_hits"`
Entries []catchDigestEntry `json:"entries"`
}
// rankCatchDigest returns catches sorted by HitCount DESC, tie-broken by ClassKey
@@ -153,12 +173,39 @@ func digestPathsHint(paths []string) string {
return s
}
// buildCatchDigest ranks every catch class and assembles the digest. now is
// injected so the render is deterministic under test. totalClasses/totalHits are
// computed over ALL classes (not just the top-N) so the checklist reports how much
// of the corpus it summarizes.
func buildCatchDigest(all []yieldledger.Catch, topN int, now time.Time) catchDigest {
ranked := rankCatchDigest(all, topN)
// buildCatchDigest partitions catch classes into ACTIONABLE (a real defect reason)
// and PLACEHOLDER (reason-less boilerplate — "pawl-review REFUTED (see evidence)", a
// bare token) via yieldledger.IsPlaceholderReason, then ranks and assembles the digest.
//
// The whole point of the digest is an ACTIONABLE pre-mortem checklist, so placeholders
// are EXCLUDED by default: injecting "watch for: pawl-review REFUTED (see evidence)" is
// pure noise. --include-placeholders (includePlaceholders=true) restores them for
// corpus auditing, but always BELOW every actionable class — real-reason classes lead,
// then placeholders, then topN truncates the combined list (so it trims placeholders
// before any actionable class). now is injected for deterministic render under test;
// TotalClasses/TotalHits are over ALL classes so the checklist reports what it filtered.
func buildCatchDigest(all []yieldledger.Catch, topN int, includePlaceholders bool, now time.Time) catchDigest {
var actionable, placeholders []yieldledger.Catch
for _, c := range all {
if yieldledger.IsPlaceholderReason(c.Reason) {
placeholders = append(placeholders, c)
} else {
actionable = append(actionable, c)
}
}
var ranked []yieldledger.Catch
if includePlaceholders {
// Actionable classes ALWAYS lead; placeholders trail. topN caps the combined
// list, so it trims placeholders before it ever drops an actionable class.
ranked = append(rankCatchDigest(actionable, 0), rankCatchDigest(placeholders, 0)...)
if topN > 0 && len(ranked) > topN {
ranked = ranked[:topN]
}
} else {
ranked = rankCatchDigest(actionable, topN)
}
totalHits := 0
for _, c := range all {
totalHits += c.HitCount
@@ -174,14 +221,18 @@ func buildCatchDigest(all []yieldledger.Catch, topN int, now time.Time) catchDig
Beads: c.Beads,
AffectedPaths: c.AffectedPaths,
WatchFor: catchWatchFor(c),
Placeholder: yieldledger.IsPlaceholderReason(c.Reason),
})
}
return catchDigest{
GeneratedAt: now.UTC().Format(time.RFC3339),
TopN: topN,
TotalClasses: len(all),
TotalHits: totalHits,
Entries: entries,
GeneratedAt: now.UTC().Format(time.RFC3339),
TopN: topN,
TotalClasses: len(all),
ActionableClasses: len(actionable),
PlaceholderClasses: len(placeholders),
IncludePlaceholders: includePlaceholders,
TotalHits: totalHits,
Entries: entries,
}
}
@@ -213,14 +264,35 @@ func renderCatchDigest(d catchDigest) []byte {
b.WriteString("and is kept separate from the human-curated `docs/gate/findings-ledger.md` (the\n")
b.WriteString("Standing Review Dimensions). **Do not hand-edit this file.**\n\n")
// Reason-less placeholder classes ("pawl-review REFUTED (see evidence)", bare "r")
// carry no defect content, so they are filtered by default (age-7758). Report the
// filtering honestly so a reader knows the corpus is larger than the checklist.
if d.PlaceholderClasses > 0 && !d.IncludePlaceholders {
fmt.Fprintf(&b, "_Filtered %d reason-less placeholder class(es) with no defect content "+
"(run `ao membrane digest --include-placeholders` to audit them)._\n\n", d.PlaceholderClasses)
}
if len(d.Entries) == 0 {
b.WriteString("_No classifiable catch classes recorded yet — clean corpus (or no data)._\n")
if d.TotalClasses == 0 {
b.WriteString("_No classifiable catch classes recorded yet — clean corpus (or no data)._\n")
} else {
// Corpus is non-empty but ENTIRELY placeholders: the honest result is an
// empty actionable checklist — the corpus needs real-reason catches to accrue.
fmt.Fprintf(&b, "_No actionable catch classes yet — all %d recorded class(es) are reason-less "+
"placeholders. The checklist becomes useful as real-reason catches accrue._\n", d.PlaceholderClasses)
}
return []byte(b.String())
}
for _, e := range d.Entries {
// One imperative line per class: "<reason> -> watch for it ...".
fmt.Fprintf(&b, "%d. **[×%d]** %s → %s\n", e.Rank, e.HitCount, e.Reason, e.WatchFor)
// One imperative line per class: "<reason> -> watch for it ...". A placeholder
// (only shown under --include-placeholders) is tagged so it is never mistaken
// for an actionable line.
tag := ""
if e.Placeholder {
tag = " _(placeholder — no defect content)_"
}
fmt.Fprintf(&b, "%d. **[×%d]** %s → %s%s\n", e.Rank, e.HitCount, e.Reason, e.WatchFor, tag)
}
b.WriteString("\nSource: `.agents/yield/yield-ledger.jsonl` (catch corpus).\n")
return []byte(b.String())
@@ -243,7 +315,7 @@ func runMembraneDigest(cmd *cobra.Command, _ []string) error {
if err != nil {
return err
}
digest := buildCatchDigest(yieldledger.DetectCatches(ledger), membraneDigestTopN, time.Now())
digest := buildCatchDigest(yieldledger.DetectCatches(ledger), membraneDigestTopN, membraneDigestIncludePlaceholders, time.Now())
abs := filepath.Join(root, filepath.FromSlash(catchDigestRelPath))
if err := writeFindingFileAtomic(abs, renderCatchDigest(digest), 0o644); err != nil {
@@ -257,13 +329,30 @@ func runMembraneDigest(cmd *cobra.Command, _ []string) error {
return enc.Encode(digest)
}
if len(digest.Entries) == 0 {
fmt.Fprintf(out, "membrane digest: no classifiable catch classes yet — wrote empty checklist to %s\n", catchDigestRelPath)
if digest.PlaceholderClasses > 0 && !digest.IncludePlaceholders {
fmt.Fprintf(out, "membrane digest: no ACTIONABLE catch classes yet — filtered %d reason-less placeholder class(es); wrote checklist to %s (--include-placeholders to audit)\n",
digest.PlaceholderClasses, catchDigestRelPath)
} else {
fmt.Fprintf(out, "membrane digest: no classifiable catch classes yet — wrote empty checklist to %s\n", catchDigestRelPath)
}
return nil
}
fmt.Fprintf(out, "membrane digest: top %d of %d recurring catch class(es) → %s\n\n",
len(digest.Entries), digest.TotalClasses, catchDigestRelPath)
denom := digest.ActionableClasses
suffix := ""
if digest.IncludePlaceholders {
denom = digest.TotalClasses
suffix = " (incl. placeholders)"
} else if digest.PlaceholderClasses > 0 {
suffix = fmt.Sprintf(" (filtered %d reason-less placeholder class(es))", digest.PlaceholderClasses)
}
fmt.Fprintf(out, "membrane digest: top %d of %d catch class(es)%s → %s\n\n",
len(digest.Entries), denom, suffix, catchDigestRelPath)
for _, e := range digest.Entries {
fmt.Fprintf(out, " %d. [×%d] %s → %s\n", e.Rank, e.HitCount, e.Reason, e.WatchFor)
tag := ""
if e.Placeholder {
tag = " (placeholder)"
}
fmt.Fprintf(out, " %d. [×%d] %s → %s%s\n", e.Rank, e.HitCount, e.Reason, e.WatchFor, tag)
}
return nil
}
+119 -4
View File
@@ -128,7 +128,7 @@ func TestRenderCatchDigest_ByteIdempotent(t *testing.T) {
d := buildCatchDigest([]yieldledger.Catch{
{ClassKey: "v1:shell/top", Domain: "shell", Reason: "unguarded cmdsub aborts under set -e", HitCount: 5, Beads: []string{"age-a", "age-b"}, AffectedPaths: []string{"scripts/x.sh"}},
{ClassKey: "v1:docs/stale", Domain: "docs", Reason: "stale retired surface referenced in shipped docs", HitCount: 2, Beads: []string{"age-c"}},
}, 10, fixedDigestClock)
}, 10, false, fixedDigestClock)
first := renderCatchDigest(d)
second := renderCatchDigest(d)
@@ -155,6 +155,121 @@ func TestRenderCatchDigest_ByteIdempotent(t *testing.T) {
}
}
// TestBuildCatchDigest_ExcludesPlaceholdersByDefault is Scenario 1 (age-7758): a
// reason-less placeholder class must NOT out-rank a real-reason class even when it has
// a HIGHER raw HitCount. By default placeholders are excluded entirely, so the real
// class leads and the placeholders are gone.
func TestBuildCatchDigest_ExcludesPlaceholdersByDefault(t *testing.T) {
in := []yieldledger.Catch{
// A placeholder with the HIGHEST hit count — the noise that dominates today.
{ClassKey: "v1:docs/pawl", Domain: "docs", Reason: "pawl-review REFUTED (see evidence)", HitCount: 25},
// A bare-token placeholder.
{ClassKey: "v1:cli/bare", Domain: "cli", Reason: "r", HitCount: 4},
// The one real, actionable class — buried at HitCount 1 under the placeholders.
{ClassKey: "v1:gates/real", Domain: "gates", Reason: "gate-routing gap: a .agents edit skips its own contract gate", HitCount: 1},
}
d := buildCatchDigest(in, 10, false, fixedDigestClock)
if d.TotalClasses != 3 {
t.Errorf("TotalClasses should report the full corpus (3), got %d", d.TotalClasses)
}
if d.PlaceholderClasses != 2 {
t.Errorf("want 2 placeholder classes reported, got %d", d.PlaceholderClasses)
}
if len(d.Entries) != 1 {
t.Fatalf("default must exclude both placeholders, keeping only the 1 real class; got %d entries: %+v", len(d.Entries), d.Entries)
}
if d.Entries[0].Reason != "gate-routing gap: a .agents edit skips its own contract gate" {
t.Errorf("the lone actionable class must lead; got %q", d.Entries[0].Reason)
}
// The placeholders (higher HitCount) must NOT appear at all.
for _, e := range d.Entries {
if yieldledger.IsPlaceholderReason(e.Reason) {
t.Errorf("placeholder reason leaked into default digest: %q", e.Reason)
}
}
}
// TestBuildCatchDigest_IncludePlaceholders is Scenario 2 (age-7758): the escape hatch
// shows EVERYTHING for corpus auditing, but real-reason classes still lead — a
// placeholder with a higher HitCount ranks BELOW every actionable class.
func TestBuildCatchDigest_IncludePlaceholders(t *testing.T) {
in := []yieldledger.Catch{
{ClassKey: "v1:docs/pawl", Domain: "docs", Reason: "pawl-review REFUTED (see evidence)", HitCount: 25},
{ClassKey: "v1:gates/real", Domain: "gates", Reason: "gate-routing gap: a .agents edit skips its own contract gate", HitCount: 1},
}
d := buildCatchDigest(in, 10, true, fixedDigestClock)
if len(d.Entries) != 2 {
t.Fatalf("--include-placeholders must show all classes; got %d", len(d.Entries))
}
// Real class first despite lower HitCount; placeholder trails.
if yieldledger.IsPlaceholderReason(d.Entries[0].Reason) {
t.Errorf("real-reason class must lead even in audit mode; entry[0]=%q", d.Entries[0].Reason)
}
if !d.Entries[1].Placeholder {
t.Errorf("the trailing entry must be flagged Placeholder=true; got %+v", d.Entries[1])
}
if d.Entries[0].Rank != 1 || d.Entries[1].Rank != 2 {
t.Errorf("ranks must be contiguous 1,2; got %d,%d", d.Entries[0].Rank, d.Entries[1].Rank)
}
}
// TestRunMembraneDigest_FiltersPlaceholdersE2E is the e2e acceptance: seed a real
// ledger mixing placeholder classes (higher hit counts) and one real-reason class,
// then assert the WRITTEN checklist leads with the actionable reason and drops the
// placeholders by default — and that --include-placeholders restores them below it.
func TestRunMembraneDigest_FiltersPlaceholdersE2E(t *testing.T) {
root := t.TempDir()
setDigestProjectDir(t, root)
// Two placeholder classes recur heavily; the one real class hits once.
seedCatch(t, root, "age-1", "docs", "pawl-review REFUTED (see evidence)", []string{"README.md"})
seedCatch(t, root, "age-2", "docs", "pawl-review REFUTED (see evidence)", []string{"docs/x.md"})
seedCatch(t, root, "age-3", "docs", "pawl-review REFUTED (see evidence)", []string{"docs/y.md"})
seedCatch(t, root, "age-4", "cli", "r", []string{"cli/a.go"})
seedCatch(t, root, "age-5", "gates", "gate-routing gap: a .agents edit skips its own contract gate", []string{"scripts/gate.sh"})
readDigest := func(includePlaceholders bool) string {
var buf bytes.Buffer
membraneDigestCmd.SetOut(&buf)
membraneDigestTopN = catchDigestDefaultTopN
membraneDigestIncludePlaceholders = includePlaceholders
if err := runMembraneDigest(membraneDigestCmd, nil); err != nil {
t.Fatalf("runMembraneDigest(include=%v): %v", includePlaceholders, err)
}
raw, err := os.ReadFile(filepath.Join(root, ".agents", "pre-mortem-checks", "catch-digest.md"))
if err != nil {
t.Fatalf("digest not written: %v", err)
}
return string(raw)
}
// Default: the actionable class leads; the placeholders are gone.
def := readDigest(false)
t.Logf("DEFAULT digest:\n%s", def)
if !strings.Contains(def, "gate-routing gap") {
t.Errorf("default digest must surface the actionable class; body:\n%s", def)
}
if strings.Contains(def, "pawl-review REFUTED") {
t.Errorf("default digest must exclude the pawl-review placeholder; body:\n%s", def)
}
// Audit: --include-placeholders restores them, but the real class still leads.
all := readDigest(true)
t.Logf("INCLUDE-PLACEHOLDERS digest:\n%s", all)
if !strings.Contains(all, "pawl-review REFUTED") {
t.Errorf("--include-placeholders must restore placeholder classes; body:\n%s", all)
}
posReal := strings.Index(all, "gate-routing gap")
posPlaceholder := strings.Index(all, "pawl-review REFUTED")
if !(posReal >= 0 && posReal < posPlaceholder) {
t.Errorf("real class must rank above placeholder even in audit mode: real=%d placeholder=%d", posReal, posPlaceholder)
}
}
// seedCatch emits one REFUTED catch verdict into root's yield ledger via the
// production Writer — the same path recall/triage read, so the fixture is the real
// persisted shape (go.md: guard-test fixtures use the production writer).
@@ -173,11 +288,11 @@ func setDigestProjectDir(t *testing.T, root string) {
t.Helper()
origProjectDir := testProjectDir
testProjectDir = root
origTop, origJSON := membraneDigestTopN, membraneDigestJSON
membraneDigestTopN, membraneDigestJSON = catchDigestDefaultTopN, false
origTop, origJSON, origIncl := membraneDigestTopN, membraneDigestJSON, membraneDigestIncludePlaceholders
membraneDigestTopN, membraneDigestJSON, membraneDigestIncludePlaceholders = catchDigestDefaultTopN, false, false
t.Cleanup(func() {
testProjectDir = origProjectDir
membraneDigestTopN, membraneDigestJSON = origTop, origJSON
membraneDigestTopN, membraneDigestJSON, membraneDigestIncludePlaceholders = origTop, origJSON, origIncl
membraneDigestCmd.SetOut(nil)
})
}
+4 -3
View File
@@ -2506,9 +2506,10 @@ ao membrane digest [--top N] [--json] [flags]
**Flags:**
```
-h, --help help for digest
--json Also print the ranked digest as JSON (the checklist file is written either way)
--top int How many top recurring catch classes to include (default 10)
-h, --help help for digest
--include-placeholders Include reason-less placeholder classes (e.g. "pawl-review REFUTED (see evidence)") for corpus auditing; excluded by default so the checklist stays actionable
--json Also print the ranked digest as JSON (the checklist file is written either way)
--top int How many top recurring catch classes to include (default 10)
```
#### `ao membrane recall`
+58
View File
@@ -167,6 +167,64 @@ func normalizeReason(reason string) string {
return strings.Join(kept, "-")
}
// placeholderReasonTokens are the disposition/route boilerplate a reason-less pawl
// REFUTE stamps ("pawl-review REFUTED (see evidence)"). They carry NO defect content.
// Kept SEPARATE from reasonStopwords: those are English glue; these are membrane-
// verdict words that a real defect sentence would never consist of ENTIRELY.
var placeholderReasonTokens = map[string]bool{
"pawl": true, "review": true, "refuted": true, "confirmed": true,
"see": true, "evidence": true,
}
// placeholderMaxRawLen is the raw-length floor below which a reason cannot carry any
// defect content — it catches bare single-token reasons ("r") the token pass keeps.
const placeholderMaxRawLen = 2
// pawlBoilerplateReason matches the pawl's own verdict STAMP as a reason — a reason
// that BEGINS "pawl-review REFUTED/CONFIRMED …" (whatever bead id or "(see evidence)"
// trails it). This is never a defect description; a real reason names the DEFECT
// ("gate-routing gap …", "missing t.Cleanup …"), never the verdict. Anchoring here
// catches the DIGIT-LESS bead-id variants ("… for age-landq-self (see evidence)") that
// stripBeadRefs deliberately leaves intact, without touching any real defect sentence.
var pawlBoilerplateReason = regexp.MustCompile(`(?i)^\s*pawl[- ]review\s+(refuted|confirmed)\b`)
// IsPlaceholderReason reports whether a catch reason is a NON-substantive placeholder:
// a reason-less pawl verdict ("pawl-review REFUTED (see evidence)"), a bare token
// ("r"), the bare disposition word ("REFUTED"), or anything that reduces to disposition
// boilerplate + bead-id refs with no defect content. Such a reason names no defect, so
// a pre-mortem checklist built from it is pure noise — `ao membrane digest` filters
// these by default (age-7758).
//
// CONSERVATIVE by construction — it must NEVER mis-flag a real defect sentence:
// - it reuses the SAME normalization the class key uses (lowercase, strip bead-id
// refs via stripBeadRefs, drop stopwords), so incidental glue never counts; and
// - it declares a placeholder only when ZERO substantive tokens survive — a token is
// substantive unless it is disposition boilerplate or a residual bead-id/version
// fragment (contains a digit). One real content word ("unguarded", "gate-routing",
// "fail") is enough to make the whole reason substantive.
func IsPlaceholderReason(reason string) bool {
if len(strings.TrimSpace(reason)) <= placeholderMaxRawLen {
return true // "", "r" — too short to carry any defect content
}
if pawlBoilerplateReason.MatchString(reason) {
return true // the pawl verdict stamp itself — no defect content, any bead id
}
normalized := normalizeReason(reason) // lowercase, strip bead refs, drop stopwords
if normalized == "" {
return true // all-stopword / all-bead-ref reason
}
for _, tok := range strings.Split(normalized, "-") {
if tok == "" || placeholderReasonTokens[tok] {
continue
}
if strings.ContainsAny(tok, "0123456789") {
continue // residual bead-id / version fragment, not defect content
}
return false // a surviving content token → substantive
}
return true
}
// slugify lowercases and collapses any run of non-alphanumerics to a single '-',
// trimming leading/trailing '-'. An empty input yields "".
func slugify(s string) string {
+46
View File
@@ -432,3 +432,49 @@ func TestDetectCatches_RoundCollapse(t *testing.T) {
t.Fatalf("want 2 distinct beads recorded, got %v", c2[0].Beads)
}
}
// TestIsPlaceholderReason is the placeholder-detector contract (age-7758): a
// reason-less pawl verdict / bare token / disposition boilerplate is a placeholder
// (true); a substantive defect sentence is NOT (false). The false cases are the
// load-bearing guard — mis-flagging a real reason would silently drop actionable
// content from the digest.
func TestIsPlaceholderReason(t *testing.T) {
placeholders := []string{
"pawl-review REFUTED (see evidence)",
"pawl-review REFUTED for age-55qz.2 (see evidence)",
"pawl-review REFUTED for age-focus-membrane-bookkeeper-m1wg.13 (see evidence)",
// DIGIT-LESS bead id: stripBeadRefs deliberately leaves "age-landq-self" intact,
// so the pawl-stamp anchor is what catches this one (regression for the miss the
// real ledger surfaced).
"pawl-review REFUTED for age-landq-self (see evidence)",
"REFUTED",
"r",
"",
" ",
"see evidence",
"pawl review confirmed",
}
for _, r := range placeholders {
if !IsPlaceholderReason(r) {
t.Errorf("IsPlaceholderReason(%q) = false, want true (non-substantive placeholder)", r)
}
}
// GUARD: real defect sentences must NEVER be flagged as placeholders.
substantive := []string{
"unguarded cmdsub aborts under set -e",
"stale retired surface referenced in shipped docs",
"missing t.Cleanup restore of shared global",
"gate-routing gap: deterministic check existed but the contract glob skips .agents edits",
"off-by-one in the wave slice boundary drops the last bead",
"fail-open: bd resolution error swallowed, gate passes on empty ledger",
// A real defect that MENTIONS the pawl mid-sentence must NOT be flagged — the
// anchor only fires on a reason that BEGINS with the verdict stamp.
"pawl service degraded so the review silently fell back to a warm cache",
}
for _, r := range substantive {
if IsPlaceholderReason(r) {
t.Errorf("IsPlaceholderReason(%q) = true, want false (real defect sentence mis-flagged)", r)
}
}
}