feat(linkedin-studio): extend §17 de-niche guard to content-planner + content-framework (B-S2 follow-up) [skip-docs]

The de-niche sweep (B-S1/B-S2) stripped the hardcoded KTG beat
(Microsoft|Azure|Copilot|public sector|offentlig sektor) from three surfaces,
but §17 only locked agents/trend-spotter.md against its return. Generalize the
guard to the full de-niched set so a beat token cannot creep back into the
other two:
  - agents/trend-spotter.md          (B-S1)
  - agents/content-planner.md        (B-S2a)
  - references/content-framework.md  (B-S2a)

Single shared NICHE_TOKENS criterion + non-vacuity/false-positive self-test
(unchanged shape), then a per-file grep loop over the explicit allowlist.
Scoped per file BY DESIGN, not tree-wide: references/content-angles.md keeps
"Public Sector" as 1 of 6 example industry tables (a KEEP surface — generic
illustration, not a beat) and is deliberately NOT guarded; "AI" is the plugin's
own subject, not a niche token. Added an "AI-driven content planning ..."
negative probe to lock that in (self-test now 5 caught / 6 ignored).

Non-vacuity proven by injection: a beat token appended to each new file makes
the guard fail naming the file+line; reverts clean. Gate 87→89/0/0 (+2 file
checks; assertion-count floor §18 still satisfied).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 12:02:01 +02:00
commit 9f9c4bbd86

View file

@ -695,23 +695,28 @@ fi
echo ""
# --- Section 17: Trend-Spotter De-Niche Guard (B-S1) ---
echo "--- Trend-Spotter De-Niche ---"
# --- Section 17: De-Niche Guard (B-S1 + B-S2) ---
echo "--- De-Niche Guard ---"
# The trend-spotter agent states its own contract — "the niche lives in the source
# list and the user's pillars, never in this agent" (Source Scanning Framework). For
# that to be true the agent file must name NO specific vendor or sector beat: the
# domain comes from the user's profile/pillars at runtime, never hardcoded here
# (plugin-is-domain-general). Pre-B-S1 the file contradicted itself, hardcoding the
# Microsoft/public-sector beat in its description, mission, trigger table and
# relevance filter. This guard forbids those KTG-beat proper nouns from returning to
# agents/trend-spotter.md. Scoped to this one agent by design (B-S1); the wider
# de-niche sweep (B-S2) covers the other surfaces. Non-vacuity self-test mirrors
# Sections 8/13: the criterion must catch the beat tokens and ignore generic prose
# (incl. the content-framework.md reference filename — B-S2a recast + renamed it
# from ai-content-framework.md, de-nicheing the file the agent reads).
# Case-insensitive: "Public sector" and "public sector" name the same beat, and a
# future reintroduction could use either case — the positive set locks that in.
# The de-niche sweep removed the hardcoded KTG beat (Microsoft / public-sector) from
# three surfaces so the domain comes from the user's profile/pillars at runtime, never
# baked in (plugin-is-domain-general):
# - agents/trend-spotter.md (B-S1: pillar-driven scanning; the agent's own
# contract says "the niche lives in the source
# list and the user's pillars, never in this agent")
# - agents/content-planner.md (B-S2a: planner calendar recast off the beat)
# - references/content-framework.md (B-S2a: recast + renamed from ai-content-framework.md)
# Each must name NO specific vendor or sector beat; this guard forbids those KTG-beat
# proper nouns from returning to any of them. Scoped per file BY DESIGN: the check runs
# against this explicit allowlist, NOT the whole tree, because references/content-angles.md
# legitimately lists "Public Sector" as one of six example industry tables (a KEEP surface,
# generic illustration — not a hardcoded beat), and "AI" is the plugin's own subject, not a
# niche token. Non-vacuity self-test mirrors Sections 8/13: the criterion must catch the beat
# tokens and ignore generic prose (incl. the content-framework.md reference filename — B-S2a
# recast + renamed it from ai-content-framework.md — and "AI"-flavoured planner prose, locking
# in that "AI" is never treated as a beat). Case-insensitive: "Public sector" and "public
# sector" name the same beat, and a future reintroduction could use either case — the
# positive set locks that in.
NICHE_TOKENS='Microsoft|Azure|Copilot|public sector|offentlig sektor'
TS_SELFTEST_OK=1
while IFS= read -r probe; do
@ -737,20 +742,23 @@ sector milestones in the user's domain
the user's content pillars and expertise areas
references/content-framework.md
major product/model releases
AI-driven content planning across the user's pillars
NEGATIVE17
if [ "$TS_SELFTEST_OK" -eq 1 ]; then
pass "trend-spotter de-niche self-test: 5 beat tokens caught (case-insensitive), 5 generic forms ignored"
pass "de-niche self-test: 5 beat tokens caught (case-insensitive), 6 generic forms ignored (incl. 'AI' kept as the plugin's own subject)"
else
fail "trend-spotter de-niche self-test failed — the guard no longer enforces the no-beat criterion"
fail "de-niche self-test failed — the guard no longer enforces the no-beat criterion"
fi
TS_NICHE_HITS=$(grep -niE "$NICHE_TOKENS" agents/trend-spotter.md 2>/dev/null || true)
if [ -z "$TS_NICHE_HITS" ]; then
pass "trend-spotter names no hardcoded vendor/sector beat (domain comes from pillars)"
for guarded in agents/trend-spotter.md agents/content-planner.md references/content-framework.md; do
NICHE_HITS=$(grep -niE "$NICHE_TOKENS" "$guarded" 2>/dev/null || true)
if [ -z "$NICHE_HITS" ]; then
pass "$guarded names no hardcoded vendor/sector beat (domain comes from pillars)"
else
fail "trend-spotter hardcodes a vendor/sector beat — generalize to pillar-driven prose:"
echo "$TS_NICHE_HITS"
fail "$guarded hardcodes a vendor/sector beat — generalize to pillar-driven prose:"
echo "$NICHE_HITS"
fi
done
echo ""