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:
parent
0e95ca8cce
commit
24775f4493
8 changed files with 793 additions and 10 deletions
|
|
@ -7,14 +7,23 @@
|
|||
* node --import tsx src/cli.ts query --topics <a,b> [--store <path>] [--json]
|
||||
* node --import tsx src/cli.ts list [--since <YYYY-MM-DD>] [--limit <n>] [--store <path>] [--json]
|
||||
* node --import tsx src/cli.ts status [--store <path>] [--json]
|
||||
* echo '<raw item|batch>' | node --import tsx src/cli.ts normalize
|
||||
* echo '<scored candidates>' | node --import tsx src/cli.ts score [--mode kortform|long-form] [--threshold N]
|
||||
*
|
||||
* The capture agent (research-engine) calls `add` to fold a freshly-polled trend
|
||||
* into the store, and `query`/`list` to reason over accumulated history. The
|
||||
* polling + relevance-scoring itself lives upstream; this is the deterministic store.
|
||||
*
|
||||
* Exit code: 0 on success, 2 on usage error.
|
||||
* `normalize` + `score` (RE-R1) are the deterministic research-engine seam: both read
|
||||
* their JSON PAYLOAD FROM STDIN (so they do not overload `--json`, which stays an
|
||||
* output toggle) and print JSON to stdout. `normalize` validates raw items into the
|
||||
* canonical envelope; `score` triages scored candidates (composite/band/threshold).
|
||||
*
|
||||
* Exit code: 0 on success, 2 on usage error (incl. unparseable stdin / bad flag).
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import {
|
||||
addTrend,
|
||||
defaultStorePath,
|
||||
|
|
@ -24,6 +33,9 @@ import {
|
|||
queryByTopic,
|
||||
saveStore,
|
||||
} from "./store.js";
|
||||
import { normalizeItem, normalizeItems } from "./item.js";
|
||||
import { triage } from "./score.js";
|
||||
import type { ScoreMode } from "./score.js";
|
||||
|
||||
function parseFlags(args: string[]): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
|
|
@ -58,11 +70,29 @@ function usage(msg: string): never {
|
|||
' add --title "<t>" --url "<u>" --topics <a,b> [--source <s>] [--summary "<s>"] [--store <path>]\n' +
|
||||
" query --topics <a,b> [--store <path>] [--json]\n" +
|
||||
" list [--since <YYYY-MM-DD>] [--limit <n>] [--store <path>] [--json]\n" +
|
||||
" status [--store <path>] [--json]",
|
||||
" status [--store <path>] [--json]\n" +
|
||||
" normalize < raw-item-or-batch.json\n" +
|
||||
" score [--mode kortform|long-form] [--threshold N] < scored-candidates.json",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
/** Read the full JSON payload from stdin, or exit 2 if it is empty/unparseable. */
|
||||
function readStdinJson(): unknown {
|
||||
let raw = "";
|
||||
try {
|
||||
raw = readFileSync(0, "utf8").trim();
|
||||
} catch {
|
||||
raw = "";
|
||||
}
|
||||
if (raw.length === 0) usage("expected a JSON payload on stdin");
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
usage("stdin is not valid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
function today(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
|
@ -166,6 +196,38 @@ function main(): void {
|
|||
return;
|
||||
}
|
||||
|
||||
if (command === "normalize") {
|
||||
const payload = readStdinJson();
|
||||
const out = Array.isArray(payload) ? normalizeItems(payload) : normalizeItem(payload);
|
||||
console.log(JSON.stringify(out, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "score") {
|
||||
const mode = flags.mode && flags.mode !== "true" ? flags.mode : "kortform";
|
||||
if (mode !== "kortform" && mode !== "long-form") {
|
||||
usage('score --mode must be "kortform" or "long-form"');
|
||||
}
|
||||
let threshold = 4.0;
|
||||
if (flags.threshold && flags.threshold !== "true") {
|
||||
const t = Number.parseFloat(flags.threshold);
|
||||
if (Number.isNaN(t)) usage("--threshold must be a number");
|
||||
threshold = t;
|
||||
}
|
||||
const payload = readStdinJson();
|
||||
if (!Array.isArray(payload)) usage("score expects a JSON array of scored candidates on stdin");
|
||||
try {
|
||||
const result = triage(payload as Array<{ scores: Record<string, number> }>, {
|
||||
mode: mode as ScoreMode,
|
||||
threshold,
|
||||
});
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
usage(`scoring failed: ${(e as Error).message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
usage(command ? `unknown command: ${command}` : "no command given");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue