feat(linkedin-studio): N6 — forslags-lag i trends (angle/targetLevel/rationale/relatedIds + selected + --ids + F7/F9-felt actionability/verdict/readerQuestion/painPoint/saturation) [skip-docs]
Artikkelforslaget blir en persistert entitet (steg 2, A1-4) og godkjenningen får
et hjem (steg 3, A1-7) — før dette genererte agenten vinkel/begrunnelse som ble kastet.
Åtte additivt-valgfrie felt på TrendRecord/TrendInput/TrendItem:
- Forslag (A1-4/A1-5): angle · targetLevel (fritekst, brukerdefinert spenn — aldri enum) ·
rationale · relatedIds[] (flerkilde).
- F7 leser-side: actionability {formulated, note?} (N7-bånd-cap-gaten leser `formulated`) ·
verdict BÆRENDE/STØTTE/NYHET (lukket mekanisme-vokab).
- F9 leser-side: readerQuestion · painPoint · saturation (N7.5-sveipet fyller dem).
Ingen schema-bump: feltene er additivt-valgfrie, ingen record trenger migrering →
SCHEMA_VERSION forblir 4 (loadStore Math.max håndterer det). First-sight-persistering
(re-capture unionerer topics + re-scorer kun; klobrer aldri en triagert vinkel).
Validering: typede felt (verdict/actionability) feiler hardt ved malformert input;
fritekst/id-liste normaliseres lempelig (summary/topics-idiomet).
Livssyklus: TrendStatus += "selected" → new→selected→acted|skipped. Ny select-verb +
--ids-batch (act/skip/reset/select); partiell suksess = exit 0 + miss-rapport, all-miss = exit 2.
setStatusMany(): ren batch-mutasjon, per-id found/notFound.
Brief: forslagsfeltene rendres per kandidat (detaljert i topp-treff, kompakt token i bullets) +
ny «🚧 I produksjon»-seksjon (selected=valgt + acted=skrevet), pillar-uavhengig, deterministisk
sortert. selected/acted forlater arbeidskøen men vises i produksjons-boardet.
commands/trends.md Step 5 oppgradert: triage → Velg (select) / Skip, batchet per verb (--ids).
Trends-suite 245→266 (ny floor, +21 N6-tester). To eksisterende tester oppdatert for den
endrede brief-kontrakten (acted vises nå i I produksjon; ranking-descriptoren ekskluderer selected).
tsc rent. Roundtrip bevist: capture m/ alle felt → query → brief rendrer feltene → select → I produksjon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7SQpXJpBSNvpWaTNq1kWZ
This commit is contained in:
parent
bb0725af66
commit
65b60337fe
9 changed files with 732 additions and 33 deletions
|
|
@ -18,7 +18,7 @@ import { homedir } from "node:os";
|
|||
import { createHash } from "node:crypto";
|
||||
|
||||
import { SCHEMA_VERSION } from "./types.js";
|
||||
import type { TrendStore, TrendRecord, TrendQueryHit, TrendStatus } from "./types.js";
|
||||
import type { TrendStore, TrendRecord, TrendQueryHit, TrendStatus, TrendVerdict, Actionability } from "./types.js";
|
||||
import type { TrendScore } from "./score.js";
|
||||
|
||||
export { SCHEMA_VERSION } from "./types.js";
|
||||
|
|
@ -35,6 +35,25 @@ export interface TrendInput {
|
|||
summary?: string;
|
||||
/** The persisted relevance envelope (RE-R3a), if the caller computed one. First-sight, never updated on re-capture. */
|
||||
score?: TrendScore;
|
||||
// ── N6 proposal fields (all first-sight, like source/capturedAt/publishedAt/summary). ──
|
||||
/** Proposed article angle (A1-4). */
|
||||
angle?: string;
|
||||
/** Proposed target level on the user profile's own span (free string, never a hard-coded enum). */
|
||||
targetLevel?: string;
|
||||
/** Why-now / applicability rationale (A1-4). */
|
||||
rationale?: string;
|
||||
/** Related trend ids — multi-source candidate (A1-5). */
|
||||
relatedIds?: string[];
|
||||
/** Reader-grip signal the N7 band-cap gate reads (MR-F7). */
|
||||
actionability?: Actionability;
|
||||
/** Reader-side utility verdict BÆRENDE/STØTTE/NYHET (MR-F7). */
|
||||
verdict?: TrendVerdict;
|
||||
/** Reader's own question, filled by the N7.5 demand-sweep (MR-F9). */
|
||||
readerQuestion?: string;
|
||||
/** Cost/risk/duty/tool the topic hits (MR-F9). */
|
||||
painPoint?: string;
|
||||
/** Market saturation judgment (MR-F9). */
|
||||
saturation?: string;
|
||||
}
|
||||
|
||||
export interface AddResult {
|
||||
|
|
@ -131,8 +150,9 @@ export function addTrend(store: TrendStore, input: TrendInput): AddResult {
|
|||
let mutated = changed;
|
||||
// RE-R3b: re-score on re-capture (last-wins). `score` is the ONE mutable field — a fresh
|
||||
// judgment (timing decays) replaces the stored one; the JSON compare avoids a false-merge
|
||||
// on an identical re-score. Provenance (source/capturedAt/publishedAt) and lifecycle
|
||||
// (status/surfacedCount/lastSurfacedAt) are untouched.
|
||||
// on an identical re-score. Provenance (source/capturedAt/publishedAt), lifecycle
|
||||
// (status/surfacedCount/lastSurfacedAt), AND the N6 proposal fields are all first-sight —
|
||||
// untouched here, so re-capture never clobbers an operator's triaged angle/verdict.
|
||||
if (input.score !== undefined && JSON.stringify(existing.score) !== JSON.stringify(input.score)) {
|
||||
existing.score = input.score;
|
||||
mutated = true;
|
||||
|
|
@ -149,6 +169,16 @@ export function addTrend(store: TrendStore, input: TrendInput): AddResult {
|
|||
topics: [...input.topics],
|
||||
...(input.summary !== undefined ? { summary: input.summary } : {}),
|
||||
...(input.score !== undefined ? { score: input.score } : {}),
|
||||
// N6 proposal fields — conditional-spread (key omitted when absent), mirroring the idiom above.
|
||||
...(input.angle !== undefined ? { angle: input.angle } : {}),
|
||||
...(input.targetLevel !== undefined ? { targetLevel: input.targetLevel } : {}),
|
||||
...(input.rationale !== undefined ? { rationale: input.rationale } : {}),
|
||||
...(input.relatedIds !== undefined ? { relatedIds: [...input.relatedIds] } : {}),
|
||||
...(input.actionability !== undefined ? { actionability: input.actionability } : {}),
|
||||
...(input.verdict !== undefined ? { verdict: input.verdict } : {}),
|
||||
...(input.readerQuestion !== undefined ? { readerQuestion: input.readerQuestion } : {}),
|
||||
...(input.painPoint !== undefined ? { painPoint: input.painPoint } : {}),
|
||||
...(input.saturation !== undefined ? { saturation: input.saturation } : {}),
|
||||
};
|
||||
store.trends.push(trend);
|
||||
return { store, added: true, merged: false };
|
||||
|
|
@ -177,6 +207,30 @@ export function setStatus(
|
|||
return { store, found: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the same status on a batch of ids in one pass (N6, A1-9 — ten candidates, one call).
|
||||
* Partitions the ids into `found` (matched + mutated) and `notFound` (no such record), each
|
||||
* order-stable on the input. Mutates the matched records in place; unknown ids are skipped,
|
||||
* never an error. Pure (no fs) — the CLI decides the exit code from the partition (all-miss ⇒
|
||||
* usage error; any hit ⇒ success + report). Duplicate input ids collapse to one mutation.
|
||||
*/
|
||||
export function setStatusMany(
|
||||
store: TrendStore,
|
||||
ids: string[],
|
||||
status: TrendStatus,
|
||||
): { store: TrendStore; found: string[]; notFound: string[] } {
|
||||
const found: string[] = [];
|
||||
const notFound: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const id of ids) {
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
const res = setStatus(store, id, status);
|
||||
(res.found ? found : notFound).push(id);
|
||||
}
|
||||
return { store, found, notFound };
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that the given trends were surfaced in a brief on `today` (the seen-log, B4).
|
||||
* PER-DAY IDEMPOTENT: a record already surfaced on `today` is skipped, so re-running the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue