feat(linkedin-studio): SB-S3c — cross-silo id-threading + post→analytics assembler [skip-docs]
Hub-side design: the published record now carries the specifics/trends ids it was built from (additive, omit-empty → byte-backward-compatible), and a new pure assembler (scripts/brain/src/assemble.ts + `brain assemble`) joins post↔analytics by normalized title-prefix + date with honest confidence tiers (high/low/none). Answers the arc's north-star query: which raw material actually performs? (specific → post → measured analytics). All four tributaries untouched (analytics READ-only via inlined raw-JSON, no package import); profile.md grammar untouched (the fact→post link stays OUT — C-1). The repeatable --specific/--trend ingest flags collect via a new collectRepeated helper, leaving parseFlags untouched. TDD: 19 new brain tests (ingest 4 + publish 3 + assemble 8 + cli 4), all SC1–SC12. brain 113/113, gate 95/0/0, BRAIN_TESTS_FLOOR 94→113, ASSERT_BASELINE_FLOOR unchanged at 80. Light-Voyage hardened (brief-review 5 FIX · plan-critic 1 BLOCK+4 MAJOR+4 MINOR · scope-guardian ALIGNED). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
This commit is contained in:
parent
016d823f3b
commit
edd3e15ef7
12 changed files with 515 additions and 14 deletions
|
|
@ -35,6 +35,14 @@ export interface PublishedRecord {
|
|||
captured_at: string;
|
||||
/** `manual` (default) | a future connector token. */
|
||||
source: string;
|
||||
/**
|
||||
* SB-S3c — the raw material this post was built from: specifics-bank + trends
|
||||
* ids (12-hex), threaded hub-side so the post↔specific↔trend↔analytics graph is
|
||||
* assemblable. Default `[]`; serialized only when non-empty (omit-empty keeps a
|
||||
* pre-S3c record byte-identical). Validated to 12-hex on parse + at the producer.
|
||||
*/
|
||||
specifics: string[];
|
||||
trends: string[];
|
||||
/** The verbatim post body — byte-exact, may contain anything (`]`/`|`/newlines/`---`). */
|
||||
body: string;
|
||||
}
|
||||
|
|
@ -54,8 +62,36 @@ export function serializePublishedRecord(rec: PublishedRecord): string {
|
|||
`published_date: ${rec.published_date}`,
|
||||
`captured_at: ${rec.captured_at}`,
|
||||
`source: ${rec.source}`,
|
||||
].join("\n");
|
||||
return header + SENTINEL + rec.body;
|
||||
];
|
||||
// SB-S3c: the raw-material id lines are appended AFTER `source:` and emitted ONLY
|
||||
// when non-empty — so a pre-S3c record (empty arrays) serializes to the unchanged
|
||||
// 5-line header, byte-identical (SC2). Body-only `mintContentId` is unaffected.
|
||||
if (rec.specifics.length > 0) header.push(`specifics: ${rec.specifics.join(",")}`);
|
||||
if (rec.trends.length > 0) header.push(`trends: ${rec.trends.join(",")}`);
|
||||
return header.join("\n") + SENTINEL + rec.body;
|
||||
}
|
||||
|
||||
const ID_RE = /^[0-9a-f]{12}$/;
|
||||
|
||||
/**
|
||||
* SB-S3c: read an optional comma-separated 12-hex id list from the header slice.
|
||||
* Absent key → `[]` (a NON-throwing reader — NOT `headerScalar`, which throws on a
|
||||
* missing key and would break every pre-S3c record). A present-but-malformed id
|
||||
* throws (never silently dropped).
|
||||
*/
|
||||
function headerIdList(header: string, key: string): string[] {
|
||||
const m = header.match(new RegExp(`^${key}:\\s*(.*?)\\s*$`, "m"));
|
||||
if (!m) return [];
|
||||
return m[1]
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s !== "")
|
||||
.map((id) => {
|
||||
if (!ID_RE.test(id)) {
|
||||
throw new Error(`malformed published record: bad ${key} id ${JSON.stringify(id)}`);
|
||||
}
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
function headerScalar(header: string, key: string): string {
|
||||
|
|
@ -94,6 +130,8 @@ export function parsePublishedRecord(text: string): PublishedRecord {
|
|||
published_date: headerScalar(header, "published_date"),
|
||||
captured_at: headerScalar(header, "captured_at"),
|
||||
source: headerScalar(header, "source"),
|
||||
specifics: headerIdList(header, "specifics"),
|
||||
trends: headerIdList(header, "trends"),
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
|
@ -167,13 +205,27 @@ export function ingestText(opts: {
|
|||
captured_at: string;
|
||||
source?: string;
|
||||
published_date?: string;
|
||||
specifics?: string[];
|
||||
trends?: string[];
|
||||
}): IngestResult {
|
||||
// SB-S3c producer guard: a malformed raw-material id fails fast HERE (not only on
|
||||
// a later re-parse). Empty/absent arrays skip validation, so every pre-S3c caller
|
||||
// (incl. scanInbox) is unaffected.
|
||||
const validateIds = (ids: string[] | undefined, kind: string): string[] => {
|
||||
const list = ids ?? [];
|
||||
for (const id of list) {
|
||||
if (!ID_RE.test(id)) throw new Error(`ingest: bad ${kind} id ${JSON.stringify(id)}`);
|
||||
}
|
||||
return list;
|
||||
};
|
||||
const record: PublishedRecord = {
|
||||
id: mintContentId(opts.body),
|
||||
provenance: "published",
|
||||
published_date: opts.published_date ?? opts.captured_at,
|
||||
captured_at: opts.captured_at,
|
||||
source: opts.source ?? "manual",
|
||||
specifics: validateIds(opts.specifics, "specifics"),
|
||||
trends: validateIds(opts.trends, "trends"),
|
||||
body: opts.body,
|
||||
};
|
||||
return { record, ...writePublished(record) };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue