feat(linkedin-studio): SB-S1 ingest data layer — record grammar + content-id [skip-docs]
PublishedRecord file-per-post grammar (fixed 5-line header + --- sentinel + verbatim body, no YAML) with parse∘serialize identity, and mintContentId = sha256(VERBATIM body)[:12] — byte-identity dedup so two structurally-different posts never collide (avoids the normalizeContent silent-data-loss path). parsePublishedRecord rejects a published/ record whose provenance != published (corruption signal). 10 grammar tests incl. the B1 edge battery (empty body, body starting with ---, mid-text \n---\n, header-shaped first line) + 4 id tests. Pure layer only; IO + CLI + voice-trainer wiring follow. brain 34→48 tests. 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
f549d9fdf3
commit
3e3990f36e
3 changed files with 194 additions and 0 deletions
|
|
@ -37,6 +37,21 @@ export function mintEntityId(seed: { kind: string; key: string }): string {
|
|||
.slice(0, 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* Content-hash id for an ingested published post = first 12 hex of
|
||||
* `sha256(VERBATIM body)`. Mirrors the specifics-bank `specificId` hash idiom
|
||||
* (`specifics-bank/src/bank.ts:57`) but deliberately hashes the body BYTE-EXACT —
|
||||
* NOT through `normalizeContent` (which lowercases + collapses whitespace). For a
|
||||
* full post body that normalization would map two structurally-different posts
|
||||
* (same words, different line breaks/case) to the same id, and the second would be
|
||||
* silently skipped on write — data loss of the exact gold signal SB-S1 captures.
|
||||
* Byte-identity hashing makes the id true content identity: only an identical
|
||||
* re-ingest collides; any real difference mints a distinct id (SB-S1).
|
||||
*/
|
||||
export function mintContentId(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex").slice(0, 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a raw provenance string to the single brain vocabulary. Trims +
|
||||
* lowercases, then returns the match or THROWS — provenance is load-bearing for
|
||||
|
|
|
|||
95
scripts/brain/src/ingest.ts
Normal file
95
scripts/brain/src/ingest.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* Ingest — the published-post gold signal (SB-S1).
|
||||
*
|
||||
* Captures the user's ACTUAL published posts into `ingest/published/` tagged
|
||||
* `provenance=published`, so the voice/profile-learning surface can learn from
|
||||
* human-published content ONLY (the model-collapse guard — never from ai-draft).
|
||||
*
|
||||
* This module has two halves:
|
||||
* - PURE: `PublishedRecord` + `parsePublishedRecord` / `serializePublishedRecord`
|
||||
* (a file-per-post constrained-header grammar; NO YAML dep, mirroring the brain
|
||||
* profile.md line-grammar idiom). `parse ∘ serialize = identity` (SC2).
|
||||
* - IO: `writePublished` / `ingestText` / `scanInbox` / `listPublished` — idempotent,
|
||||
* collision-safe, create-on-demand under the brain `dataRoot` (SC1/SC3/SC4).
|
||||
*
|
||||
* The record id is `mintContentId(VERBATIM body)` (id.ts) — byte-identity, so two
|
||||
* structurally-different posts never collide and the write path never silently
|
||||
* drops a differing body.
|
||||
*/
|
||||
|
||||
import { mintContentId, normalizeProvenance } from "./id.js";
|
||||
|
||||
/** One ingested published post. `provenance` is always `published` in SB-S1. */
|
||||
export interface PublishedRecord {
|
||||
/** mintContentId(body) — 12 hex, the dedupe key + filename stem. */
|
||||
id: string;
|
||||
/** Always `published` — the type pins it; parse rejects any other value. */
|
||||
provenance: "published";
|
||||
/** YYYY-MM-DD — when the post was published (operator-supplied or = captured_at). */
|
||||
published_date: string;
|
||||
/** YYYY-MM-DD — when this record was ingested. */
|
||||
captured_at: string;
|
||||
/** `manual` (default) | a future connector token. */
|
||||
source: string;
|
||||
/** The verbatim post body — byte-exact, may contain anything (`]`/`|`/newlines/`---`). */
|
||||
body: string;
|
||||
}
|
||||
|
||||
/** The one separator string, used identically by serialize and parse. */
|
||||
const SENTINEL = "\n---\n";
|
||||
|
||||
/**
|
||||
* Serialize a record to the file grammar: a fixed 5-line header, the `---`
|
||||
* sentinel, then the verbatim body. No title, no blank-line padding (the body is
|
||||
* captured byte-exact, so any padding would break the round-trip identity).
|
||||
*/
|
||||
export function serializePublishedRecord(rec: PublishedRecord): string {
|
||||
const header = [
|
||||
`id: ${rec.id}`,
|
||||
`provenance: ${rec.provenance}`,
|
||||
`published_date: ${rec.published_date}`,
|
||||
`captured_at: ${rec.captured_at}`,
|
||||
`source: ${rec.source}`,
|
||||
].join("\n");
|
||||
return header + SENTINEL + rec.body;
|
||||
}
|
||||
|
||||
function headerScalar(header: string, key: string): string {
|
||||
const m = header.match(new RegExp(`^${key}:\\s*(.*?)\\s*$`, "m"));
|
||||
if (!m) throw new Error(`malformed published record: missing "${key}:" header`);
|
||||
return m[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a record back from the file grammar. Splits on the FIRST `---` sentinel
|
||||
* (the header lines are constrained `key: value` pairs that never contain one), so
|
||||
* a body that itself contains `\n---\n` is preserved verbatim. The header scalars
|
||||
* are read only from the header slice — a header-shaped body line cannot leak in.
|
||||
* `provenance` is asserted to be `published`: a `published/` record carrying any
|
||||
* other provenance is a corruption signal and throws (never silently accepted).
|
||||
*/
|
||||
export function parsePublishedRecord(text: string): PublishedRecord {
|
||||
const idx = text.indexOf(SENTINEL);
|
||||
if (idx === -1) throw new Error("malformed published record: no `---` separator");
|
||||
const header = text.slice(0, idx);
|
||||
const body = text.slice(idx + SENTINEL.length);
|
||||
|
||||
const id = headerScalar(header, "id");
|
||||
if (!/^[0-9a-f]{12}$/.test(id)) {
|
||||
throw new Error(`malformed published record: bad id ${JSON.stringify(id)}`);
|
||||
}
|
||||
const provenance = normalizeProvenance(headerScalar(header, "provenance"));
|
||||
if (provenance !== "published") {
|
||||
throw new Error(
|
||||
`corrupt published record: provenance is "${provenance}", expected "published"`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
id,
|
||||
provenance,
|
||||
published_date: headerScalar(header, "published_date"),
|
||||
captured_at: headerScalar(header, "captured_at"),
|
||||
source: headerScalar(header, "source"),
|
||||
body,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue