feat(linkedin-studio): RE-R1 — item-schema (B1) + triage-scorer (B2) as tested code behind CLI seam [skip-docs]

Lift the research engine's deterministic core out of agents/trend-spotter.md prose
into pure, tested TypeScript under scripts/trends/, behind a CLI seam the agent calls.

- B1 src/item.ts: TrendItem ingress envelope + normalizeItem/normalizeItems
  (required-field validation, topic normalize+dedupe via store's normalizeField,
  optional publishedAt ISO-validate). No id (store derives it); no store bridge
  (capturedAt injection is R2).
- B2 src/score.ts: per-mode weight consts mirroring the SSOT
  (references/trend-scoring-modes.md), composite (weighted sum, [1,10] guard),
  band (5-band map + exact SSOT action strings), triage (keep>=threshold, rank desc,
  annotate composite+band). Owns ONLY the arithmetic; the five judgment scores stay
  model-side.
- CLI normalize/score: JSON payload on STDIN, JSON to stdout (the existing --json
  output toggle is untouched); exit 2 on bad invocation, 0 otherwise.
- Wire trend-spotter.md to name 'src/cli.ts score' as the deterministic-step owner
  (prose pointer; the agent still supplies the five scores). Domain-general.
- Gate: TRENDS_TESTS_FLOOR 24->62; new unconditional Section 16g (score.ts both-mode
  weight-sets + trend-spotter scorer-pointer + non-vacuity self-test);
  ASSERT_BASELINE_FLOOR 84->87.

TDD: logic-RED proven (33/34 item+score fail on assertions, not module-not-found),
then GREEN (trends suite 62/62); CLI RED 2/4 -> GREEN 4/4. Full gate 102/0/0.
No store-schema change (SCHEMA_VERSION stays 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
This commit is contained in:
Kjell Tore Guttormsen 2026-06-24 10:09:45 +02:00
commit 24775f4493
8 changed files with 793 additions and 10 deletions

View file

@ -38,7 +38,10 @@
# inferences', with a non-vacuity self-test) in Section 16e; the brain reconcile-wiring
# guard (SB-S3e: scripts/brain/src/cli.ts dispatches `reconcile` AND calls the core
# reconcileRecentPosts by literal name, with a non-vacuity self-test) in Section 16f;
# the assertion-count anti-erosion floor (SC6) in Section 18. All are live below (Sections 818).
# the trends-scorer wiring guard (RE-R1: scripts/trends/src/score.ts encodes both mode
# weight-sets AND agents/trend-spotter.md references the scorer CLI 'src/cli.ts score',
# with a non-vacuity self-test) in Section 16g; the assertion-count anti-erosion floor
# (SC6) in Section 18. All are live below (Sections 818).
#
# Usage: bash scripts/test-runner.sh
# bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`.
@ -689,7 +692,7 @@ if [ -x "$TR_DIR/node_modules/.bin/tsx" ]; then
TR_OUT=$( set +e; (cd "$TR_DIR" && npm test) 2>&1; echo "TR_EXIT:$?" )
TR_EXIT=$(echo "$TR_OUT" | grep -oE 'TR_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
TR_TESTS=$(echo "$TR_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
TRENDS_TESTS_FLOOR=24 # B-S3: +3 newestCaptureDate tests (staleness signal)
TRENDS_TESTS_FLOOR=62 # store 24 + RE-R1: item 18 + score 16 + cli 4 (item-schema + triage-scorer)
if [ "$TR_EXIT" = "0" ] && [ -n "$TR_TESTS" ] && [ "$TR_TESTS" -ge "$TRENDS_TESTS_FLOOR" ]; then
pass "trends-store suite green: $TR_TESTS tests pass (floor $TRENDS_TESTS_FLOOR)"
else
@ -1002,6 +1005,70 @@ done
echo ""
# --- Section 16g: Trends Scorer Wiring (research-engine RE-R1 / B2) ---
echo "--- Trends Scorer Wiring ---"
# RE-R1 lifts the composite/band/threshold arithmetic out of trend-spotter.md prose into
# tested code (scripts/trends/src/score.ts) behind a CLI seam. Two literals must hold,
# grepped EXACT (grep -F), deps-absent-safe (pure grep, no tsx):
# (1) score.ts encodes BOTH mode weight-sets (the 'kortform' + 'long-form' literals), so a
# silent collapse to one mode fails here (the per-mode arithmetic itself is unit-tested
# in score.test.ts, behind the deps guard / trends-suite floor);
# (2) agents/trend-spotter.md references the scorer CLI by the literal 'src/cli.ts score' —
# the lift is real and grep-able, not merely documented.
# Non-vacuity self-test mirrors Sections 16c-17: the weight-set predicate (AND of both mode
# literals) must accept a both-modes probe and reject single-mode probes; the wiring predicate
# must accept a probe carrying the scorer-pointer literal and reject one without it. Labelled
# 16g but placed after Section 17 / before Section 18 (anti-erosion must run last so it sees
# every prior check). UNCONDITIONAL (no tsx) -> counts toward ASSERT_BASELINE_FLOOR.
WEIGHT_KORT_LIT='kortform'
WEIGHT_LONG_LIT='long-form'
SCORER_WIRE_LIT='src/cli.ts score'
weights_both_modes() { # $1 = text; true iff BOTH mode literals present (echo twice — grep consumes stdin)
echo "$1" | grep -qF "$WEIGHT_KORT_LIT" && echo "$1" | grep -qF "$WEIGHT_LONG_LIT"
}
G16_SELFTEST_OK=1
if ! weights_both_modes 'the kortform weight-set and the long-form weight-set are both encoded'; then
G16_SELFTEST_OK=0; echo " non-vacuity FAIL: a both-modes weight probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if weights_both_modes "$probe"; then
G16_SELFTEST_OK=0; echo " false-positive FAIL: single-mode weight probe accepted -> $probe"
fi
done <<'NEGATIVE16G'
only the kortform weight-set is present here
only the long-form weight-set is present here
NEGATIVE16G
if ! echo 'pipe the scores to src/cli.ts score for the composite' | grep -qF "$SCORER_WIRE_LIT"; then
G16_SELFTEST_OK=0; echo " non-vacuity FAIL: a wired scorer-pointer probe was not detected"
fi
if echo 'the agent computes the composite itself' | grep -qF "$SCORER_WIRE_LIT"; then
G16_SELFTEST_OK=0; echo " false-positive FAIL: an unwired probe matched the scorer pointer"
fi
if [ "$G16_SELFTEST_OK" -eq 1 ]; then
pass "trends-scorer self-test: both-modes weight predicate + scorer-pointer predicate detect wiring, reject the under-wired forms"
else
fail "trends-scorer self-test failed — the scorer-wiring lint is vacuous or over-eager"
fi
SCORE_TS="scripts/trends/src/score.ts"
if grep -qF "$WEIGHT_KORT_LIT" "$SCORE_TS" 2>/dev/null && grep -qF "$WEIGHT_LONG_LIT" "$SCORE_TS" 2>/dev/null; then
pass "score.ts encodes both mode weight-sets ('$WEIGHT_KORT_LIT' + '$WEIGHT_LONG_LIT')"
else
fail "score.ts missing a mode weight-set — needs both '$WEIGHT_KORT_LIT' and '$WEIGHT_LONG_LIT' in $SCORE_TS"
fi
if grep -qF "$SCORER_WIRE_LIT" agents/trend-spotter.md; then
pass "trend-spotter.md references the scorer CLI ('$SCORER_WIRE_LIT') as the deterministic-step owner"
else
fail "trend-spotter.md does not reference the scorer CLI — add a '$SCORER_WIRE_LIT' pointer (RE-R1 lift)"
fi
echo ""
# --- Section 18: Assertion-Count Anti-Erosion (SC6) ---
# The lint self-modifies its own checks, so a green run could mask a silently dropped
# assertion. Pin the total pass()+fail() invocations as a monotonic floor; the count
@ -1011,12 +1078,14 @@ echo ""
# +2 for SB-S3a's two UNCONDITIONAL Section-16d checks (profile-reader self-test +
# strategy-advisor wiring grep) = 80; +2 for SB-S3d's two UNCONDITIONAL Section-16e
# checks (ops-reader self-test + strategy-advisor ops-wiring grep) = 82; +2 for SB-S3e's
# two UNCONDITIONAL Section-16f checks (reconcile self-test + brain-CLI reconcile grep) = 84.
# two UNCONDITIONAL Section-16f checks (reconcile self-test + brain-CLI reconcile grep) = 84;
# +3 for RE-R1's three UNCONDITIONAL Section-16g checks (trends-scorer self-test + score.ts
# both-modes weight-set grep + trend-spotter scorer-pointer grep) = 87.
# NB: the floor tracks the deps-absent MINIMUM (conditional TS suites warn-skip and drop
# the count), so it is bumped only by UNCONDITIONAL new checks — NOT pinned to the
# deps-present TOTAL_CHECKS (that would zero the warn-skip margin and false-fail a fresh
# clone). Runs last so TOTAL_CHECKS sees every prior check.
ASSERT_BASELINE_FLOOR=84
ASSERT_BASELINE_FLOOR=87
TOTAL_CHECKS=$((PASS + FAIL))
if [ "$TOTAL_CHECKS" -ge "$ASSERT_BASELINE_FLOOR" ]; then
pass "assertion-count anti-erosion: $TOTAL_CHECKS checks >= baseline floor $ASSERT_BASELINE_FLOOR"