mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
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:
+115
-26
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user