feat(linkedin-studio): N13 — publiserings-slots + vacancy-varsel + WIP-roll-up + slot-default [skip-docs]

A1-10 + D-5. The queue answers "what is scheduled"; nothing answered "is the week
covered, and what is still being written". N13 makes both readable at session start.

- Publishing slots are operator config: profile/publishing-slots.json in the per-user
  data dir, schema + OPT-IN template in config/publishing-slots.template.json. The
  plugin ships no publishing times and writes the grid for nobody; absent config means
  every slot surface stays silent (same never-nag discipline as the trend/brain nudges).
  Data dir rather than the state file, which is machine-written and whose frontmatter
  reader does not parse lists.
- hooks/scripts/slots.mjs (new): one implementation behind four surfaces. Zero-dep (a
  SessionStart hook must not spawn tsx), pure core (dates in as strings, no clock),
  imported by the commands exactly as queue-manager.mjs already is — so session-start's
  vacancy warning and Step 10's slot default cannot drift apart.
- Coverage counts the short-form queue (scheduled/published; cancelled frees the slot)
  AND editions-register rows claiming that date, counted per date, so long-form and
  short-form cannot be double-booked and a two-slot day needs two posts.
- Session start gains "## Editions in Flight" (series, phase, next action, claimed slot,
  days in flight) and "## Publishing Slots" (next open slot, open count in 14 days, and
  how to fill it when the gap is <=3 days). The existing discovery nudge is untouched.
- /linkedin router + /linkedin:calendar surface the same roll-up; calendar documents the
  opt-in copy. /linkedin:newsletter Step 10 defaults to the next open slot (one
  confirmation, not an interview) and writes --slot onto the register row.
- D-5: references/scheduling-strategy.md marked "confidence: low / directional" per the
  algorithm reference's own standard, deferring to the operator's grid and their own
  analytics. Neutral example time replaces the operator-specific one in the N12 docs.

TDD: red proven first (module absent; 5 session-start behaviours failing).
hooks 140 -> 174 · test-runner 184 -> 197 (Section 16t: 13 unconditional greps incl. a
de-niche check that no publishing time is hardcoded in the slot path and no operator grid
is committed, + non-vacuity self-test; anti-erosion floor 166 -> 179).
Suites green: trends 300/0 · brain 134/0 · editions 72/0 · specifics-bank 45/0 ·
tests 35/0 · render 60/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 07:28:48 +02:00
commit 677eab9294
14 changed files with 1069 additions and 19 deletions

View file

@ -2057,6 +2057,143 @@ fi
echo ""
# --- Section 16t: Publishing Slots + WIP Surfacing (N13 / A1-10) ---
echo "--- Publishing Slots (N13) ---"
# N13 answers the question a queue cannot: is the WEEK covered? The grid of publishing
# slots is operator config, coverage is queue + editions register, and the surfaces that
# report it (session start, router, calendar) plus the one that consumes it (newsletter
# Step 10 slot default) must all read the SAME module - two implementations of "next free
# slot" would disagree within a week, and the operator would learn to trust neither.
# (A1-10 wiring) a WIP surface loads slots.mjs and asks it for BOTH vacancy and
# in-flight editions - a compound predicate, so a non-vacuity
# self-test guards it (mirrors Sections 16p/16q/16r/16s).
# (A1-10 consume) Step 10 defaults to the next free slot AND claims it on the row.
# (A1-10 placement) the grid + register resolve under the per-user data dir (M0).
# (D-5) the shipped scheduling times are marked low/directional and defer
# to the operator's own grid.
# (de-niche) the plugin ships NO publishing times in the slot code path and no
# operator grid - the times are user config, always.
SLOTS_SRC="hooks/scripts/slots.mjs"
SS_SRC="hooks/scripts/session-start.mjs"
SLOTS_TPL="config/publishing-slots.template.json"
NL_N13="commands/newsletter.md"
SCHED_REF="references/scheduling-strategy.md"
slots_surface_wired() { # $1 = text; wired iff it LOADS the shared module and asks for BOTH vacancy and in-flight
echo "$1" | grep -qF "slots.mjs" \
&& echo "$1" | grep -qE "slotVacancies|nextFreeSlot" \
&& echo "$1" | grep -qF "activeEditions"
}
SL_SELFTEST_OK=1
if ! slots_surface_wired "import { slotVacancies, activeEditions } from hooks/scripts/slots.mjs and print both"; then
SL_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired slot-surface probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if slots_surface_wired "$probe"; then
SL_SELFTEST_OK=0; echo " false-positive FAIL: under-wired slot-surface probe accepted -> $probe"
fi
done <<'NEGATIVE16T'
import activeEditions from slots.mjs and list what is in production
call slotVacancies and activeEditions from somewhere, no module named
import slotVacancies from slots.mjs and report open slots only
NEGATIVE16T
if [ "$SL_SELFTEST_OK" -eq 1 ]; then
pass "slot-surface self-test: predicate needs slots.mjs + vacancy + activeEditions (1 accepted, 3 under-wired rejected)"
else
fail "slot-surface self-test failed - the N13 wiring lint is vacuous or over-eager"
fi
# (A1-10 wiring) the two WIP surfaces report capacity, not just a queue
if slots_surface_wired "$(cat commands/linkedin.md)"; then
pass "router surfaces work-in-progress + open slots from the shared module (A1-10)"
else
fail "commands/linkedin.md does not surface in-flight editions and open slots - the week stays invisible (A1-10)"
fi
if slots_surface_wired "$(cat commands/calendar.md)"; then
pass "calendar surfaces the slot grid + editions in flight from the shared module (A1-10)"
else
fail "commands/calendar.md does not read slots.mjs for vacancy + in-flight editions (A1-10)"
fi
# (A1-10 opt-in) the grid is offered, never written for the user
if grep -qF "publishing-slots.template.json" commands/calendar.md; then
pass "calendar documents the opt-in grid copy - the plugin imposes no schedule (A1-10/de-niche)"
else
fail "commands/calendar.md never points at the slot-grid template - the feature is undiscoverable (A1-10)"
fi
# (A1-10 session start) both blocks render at session start, from the same module
if grep -qF "from './slots.mjs'" "$SS_SRC" \
&& grep -qF "## Editions in Flight" "$SS_SRC" \
&& grep -qF "## Publishing Slots" "$SS_SRC"; then
pass "session start renders in-flight editions + publishing slots from slots.mjs (A1-10)"
else
fail "$SS_SRC does not render the week picture from slots.mjs - the operator must open files again (A1-10)"
fi
# (A1-10 coverage) vacancy counts BOTH short-form queue and long-form register
if grep -qF "export function coverageByDate" "$SLOTS_SRC" && grep -qF "readRegisterRows" "$SLOTS_SRC"; then
pass "slot coverage counts the queue AND the editions register - long-form cannot be double-booked (A1-10)"
else
fail "$SLOTS_SRC does not derive coverage from both the queue and the editions register (A1-10)"
fi
# (A1-10 placement) grid + register resolve under the per-user data dir (M0 convention)
if grep -qF "getDataRoot('profile')" "$SLOTS_SRC" && grep -qF "getDataRoot('editions')" "$SLOTS_SRC"; then
pass "slot grid + register resolve under the per-user data dir - they survive reinstalls (M0/A1-10)"
else
fail "$SLOTS_SRC does not resolve the grid and register under the data dir (M0/A1-10)"
fi
# (A1-10 consume) Step 10 defaults to the next free slot instead of interviewing
STEP10_N13=$(awk '/^## Step 10:/{f=1} /^## Distribution channel/{f=0} f' "$NL_N13")
if echo "$STEP10_N13" | grep -qF "nextFreeSlot"; then
pass "newsletter Step 10 defaults to the next free slot - scheduling is one confirmation (A1-10)"
else
fail "newsletter Step 10 still asks for the slot from scratch - the grid buys nothing (A1-10)"
fi
if echo "$STEP10_N13" | grep -qF -- "--slot"; then
pass "newsletter Step 10 claims the slot on the register row - the next session stops offering it (A1-10)"
else
fail "newsletter Step 10 never writes --slot - a scheduled edition still reads as an open slot (A1-10)"
fi
# (D-5) the shipped times are marked as what they are, and defer to the operator's grid
if grep -qF "low / directional" "$SCHED_REF"; then
pass "scheduling reference carries the low/directional epistemic marking (D-5)"
else
fail "$SCHED_REF presents practitioner lore as fact - mark it low / directional (D-5)"
fi
if grep -qF "publishing-slots.json" "$SCHED_REF"; then
pass "scheduling reference defers to the operator's own grid (D-5)"
else
fail "$SCHED_REF does not point at the operator grid - the tables read as the authority (D-5)"
fi
# (de-niche) the template exists and states that nothing is written for the user
if [ -f "$SLOTS_TPL" ] && grep -qF '"schemaVersion": 1' "$SLOTS_TPL" && grep -qF "OPT-IN" "$SLOTS_TPL"; then
pass "slot-grid template ships with a schema and an explicit opt-in (A1-10/de-niche)"
else
fail "$SLOTS_TPL missing, unversioned, or does not state the opt-in (A1-10/de-niche)"
fi
# (de-niche) no publishing times hardcoded in the slot code path, no operator grid committed
SLOT_TIME_LITERALS=$(grep -cE "['\"][0-2][0-9]:[0-5][0-9]['\"]" "$SLOTS_SRC" "$SS_SRC" 2>/dev/null | awk -F: '{s+=$2} END {print s+0}')
COMMITTED_GRIDS=$(find . -name "publishing-slots.json" -not -path "*/node_modules/*" 2>/dev/null | wc -l | tr -d ' ')
if [ "$SLOT_TIME_LITERALS" -eq 0 ] && [ "$COMMITTED_GRIDS" -eq 0 ]; then
pass "no publishing times hardcoded in the slot path and no operator grid committed (de-niche)"
else
fail "hardcoded slot times in the code path ($SLOT_TIME_LITERALS) or a committed operator grid ($COMMITTED_GRIDS) - times are user config (de-niche)"
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
@ -2103,12 +2240,17 @@ echo ""
# self-test + protocol-block compound grep + mirror-invariant grep + 16-phase coverage sweep +
# Step-10 register-complete grep + register.ts upsertEdition grep + register.ts completeEdition/
# listEditions grep + register.ts data-dir placement grep + editionState.ts appendPhase grep +
# template phaseLog-doc grep + template phaseLog-slot grep) = 166.
# template phaseLog-doc grep + template phaseLog-slot grep) = 166; +13 for N13's thirteen
# UNCONDITIONAL Section-16t checks (slot-surface self-test + router wiring grep + calendar
# wiring grep + calendar opt-in grep + session-start two-block compound grep + coverage
# queue+register grep + data-dir placement grep + Step-10 nextFreeSlot grep + Step-10 --slot
# grep + scheduling-ref directional grep + scheduling-ref operator-grid grep + slot-template
# schema/opt-in grep + de-niche no-hardcoded-times/no-committed-grid check) = 179.
# 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=166
ASSERT_BASELINE_FLOOR=179
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"