feat(linkedin-studio): N7.5 — MR-F9 etterspørsels-sveip (Tier-5-kilder + smertepunkt + vokabular-oversettelse + arc-kontrakt) [skip-docs]

The «innenfra og ut» demand-sweep: the mechanism that FILLS the N6 reader fields
(readerQuestion/painPoint/saturation). Discovery finds "what happened"; this layer
translates it into "the problem the reader is stuck on".

- demand-spotter agent (agents 19->20, inherits session): three passes after
  discovery, before drafting — demand-sweep -> pain-point map -> vocabulary translation.
- Tier 5 demand sources in config/trends-sources.template.md (inverse of Tier 1-4;
  honest blind spots: YouTube API, Reddit approximate, HN/GitHub ground truth).
- demand signal on TrendRecord (strength + answered) — rankable twin of the verbatim
  saturation text; additive-optional, store schema stays v4 (no migration).
- arc.ts (§4 output contract): groupIntoArcs (relatedIds transitive closure),
  rankArcQuestions (etterspørsel x kan-svare x ikke-besvart), classifyMarketGap
  (supply-gap != demand-gap != saturated). Pure + deterministic (TDD).
- arcs CLI verb + /linkedin:trends --demand mode (commands stay 30); morning brief
  shows the per-candidate demand signal.

Suites: trends 276->300, test-runner 139->140, tsc clean. Others unchanged
(brain 134, hooks 140, tests 35, render 60).

MR-F9 built (bygget-men-ubevist) — the (a)/(b)/(c) evidence gate is a runtime
demonstration, proven consumer-side (plugin agents don't resolve in the dev repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014bE7VbkmR3cqHFEeGfzgwb
This commit is contained in:
Kjell Tore Guttormsen 2026-07-23 22:26:39 +02:00
commit 9de38c4939
16 changed files with 968 additions and 8 deletions

View file

@ -13,6 +13,7 @@
* echo '<raw item|batch>' | node --import tsx src/cli.ts capture [--store <path>] [--json]
* node --import tsx src/cli.ts brief [--pillars <a,b>] [--fresh-days N] [--first-mover-days N] [--saturation-at N]
* [--out <dir>] [--no-mark] [--store <path>] [--json]
* node --import tsx src/cli.ts arcs [--topics <a,b>] [--out <dir>] [--store <path>] [--json]
* node --import tsx src/cli.ts schedule --pillars <a,b> [--at HH:MM] [--fresh-days N]
* [--platform auto|launchd|cron] [--install|--uninstall] [--store <path>]
*
@ -83,6 +84,7 @@ import {
} from "./brief.js";
import { launchdPlist, crontabLine, installInstructions, uninstallInstructions, defaultLabel } from "./schedule.js";
import type { ScheduleSpec } from "./schedule.js";
import { groupIntoArcs, renderArcs } from "./arc.js";
function parseFlags(args: string[]): Record<string, string> {
const out: Record<string, string> = {};
@ -123,6 +125,7 @@ function usage(msg: string): never {
" score [--mode kortform|long-form] [--threshold N] < scored-candidates.json\n" +
" capture [--store <path>] [--json] < raw-item-or-batch.json\n" +
" brief [--pillars <a,b>] [--fresh-days N] [--first-mover-days N] [--saturation-at N] [--out <dir>] [--no-mark] [--store <path>] [--json]\n" +
" arcs [--topics <a,b>] [--out <dir>] [--store <path>] [--json]\n" +
" schedule --pillars <a,b> [--at HH:MM] [--fresh-days N] [--platform auto|launchd|cron] [--install|--uninstall] [--store <path>]",
);
process.exit(2);
@ -418,6 +421,44 @@ function main(): void {
return;
}
if (command === "arcs") {
// The demand-sweep's §4 output (MR-F9, N7.5): group the demand-swept records into veins (arcs)
// and render the reader-side output contract — question inventory ranked on etterspørsel × kan-svare
// × ikke-besvart, per-vein market verdict (supply-gap ≠ demand-gap ≠ saturated), BÆRENDE/STØTTE order.
// Reads only records that carry a demand-side signal (the sweep populated them); an optional --topics
// narrows the pool to a theme. Print to stdout by default; --out <dir> writes a dated arc map; --json
// emits the arc structures. No store mutation (a read-only view, like brief without the surfacing write).
const store = loadStore(storePath);
const wanted = splitTopics(flags.topics).map((t) => t.toLowerCase());
const swept = store.trends.filter((t) => {
const hasSignal =
t.demand !== undefined ||
t.readerQuestion !== undefined ||
t.painPoint !== undefined ||
t.saturation !== undefined;
if (!hasSignal) return false;
if (wanted.length === 0) return true;
const have = new Set(t.topics.map((x) => x.toLowerCase()));
return wanted.some((w) => have.has(w));
});
const arcs = groupIntoArcs(swept);
if (asJson) {
console.log(JSON.stringify(arcs, null, 2));
return;
}
const md = renderArcs(arcs);
if (flags.out && flags.out !== "true") {
const outDir = flags.out;
mkdirSync(outDir, { recursive: true });
const path = join(outDir, `${today()}-arcs.md`);
writeFileSync(path, md, "utf8");
console.log(`Wrote arc map: ${path} (${arcs.length} åre(r), ${swept.length} swept record(s))`);
return;
}
process.stdout.write(md);
return;
}
if (command === "schedule") {
// RE-R3c — print-first autonomous trigger. Emits (or installs) a launchd plist / cron line firing
// the DETERMINISTIC brief daily via run-daily.sh; never runs launchctl or the cron table (C2).