Skip to content

Camp A RAG writer: product path with honest holdout GO - #44

Merged
telivity-otaip merged 23 commits into
mainfrom
camp-a-rag-writer
Jul 30, 2026
Merged

Camp A RAG writer: product path with honest holdout GO#44
telivity-otaip merged 23 commits into
mainfrom
camp-a-rag-writer

Conversation

@telivity-otaip

Copy link
Copy Markdown
Contributor

Summary

  • Ships Camp A RAG as the product write path: index-voicebuild-style-profilewrite on Qwen3.5-9B-4bit with adapter=none (no LoRA).
  • Fixes the false NO-GO harness (chat template, lossy briefs, clean corpus index, length-normalized scoring, voice-card wiring, draft trim) so holdout ear is trustworthy.
  • Holdout result: RAG 2 / base 1 / tie 0, rag_beats_base=true, no RAG disqualifications. README updated to match.

Test plan

  • pytest green (259+ Contoso-safe tests)
  • personality-protect index-voice && build-style-profile && write --topic ... --points ...
  • personality-protect eval-write-holdout with three holdout ids; receipt shows rag_beats_base
  • Confirm status reports voice_mode=rag / adapter=none
  • Confirm README shipped path is RAG, not train→filter

Made with Cursor

telivity-otaip and others added 23 commits July 29, 2026 19:34
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Compute median sentence length, short-line ratio, contractions, and you/I from selected posts, plus a banned AI-filler list, via build-style-profile.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mask retrieved entities and reject exemplar parroting or invented brief facts before drafts are accepted.

Co-authored-by: Cursor <cursoragent@cursor.com>
Build a profile-scoped index that excludes holdouts and replaces stale vectors so RAG retrieval remains private and reproducible.

Co-authored-by: Cursor <cursoragent@cursor.com>
Draft posts from retrieved exemplars on base weights: retrieve 3-5 masked
exemplars, build the locked prompt, generate, and regenerate once when the
parrot or invention guard trips. The --json receipt records adapter=none.

MLX now blocks by default and requires PP_MLX_ALLOW=1, because importing mlx
without a Metal device aborts the interpreter instead of raising. Generation is
injectable and pytest installs a meta_path finder that rejects mlx imports, so
sandboxed runs fail as ImportError rather than a crash dialog.

Co-authored-by: Cursor <cursoragent@cursor.com>
Status now reports voice_mode=rag with adapter=none, and the README
quickstart points at index-voice → build-style-profile → write instead
of the legacy translator/train path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Carve never-indexed holdouts, mine briefs, score injectable RAG and bare-base drafts, and emit an eval-write-holdout receipt without draft bodies.

Co-authored-by: Cursor <cursoragent@cursor.com>
Apply the chat template on the write path (shared with filter), mine
real terse briefs instead of leaking the holdout body, persist raw
prompts/drafts under gitignored dogfood/raw, and stop invent-guard from
flagging ordinary nouns and prompt scaffolding.

Co-authored-by: Cursor <cursoragent@cursor.com>
The holdout eval was measuring the wrong thing three ways over.

Write generate handed Qwen3.5-instruct a bare text block, so it continued
the document and echoed EXAMPLES/BRIEF headers back instead of writing a
post. Both write paths now render through a shared chat_prompt helper, the
same way filter.py already did.

Brief mining set points to the entire holdout body, which handed the eval
its answer key: the bare-base arm could paraphrase the target and win on
stylometric distance with no voice modelling at all. Briefs are now a short
topic plus terse bullets fitted to a word budget proportional to the post,
tracked as brief_leakage_ratio in the receipt and enforced fail-closed. The
invent guard scores against a separate guard_facts field so its fact set can
never silently widen beyond what the model was shown.

The invention guard flagged common nouns, calendar words, generic acronyms
and our own prompt scaffolding as fabricated companies, so it fired on every
draft and carried no signal. Masking stays strict (unknown capitalized words
are masked); invention now uses a much wider allowlist, and both directions
are tested.

Raw prompts and drafts are persisted under the profile's gitignored
dogfood/raw for human review, and stripped from CLI JSON and receipts.

Co-authored-by: Cursor <cursoragent@cursor.com>
A [ENTITY] placeholder in the prompt is an instruction the model follows: one
holdout draft came back as its own redacted exemplars with dozens of literal
[ENTITY] markers. Names are now cut out and the text tidied, so nothing in the
prompt advertises a slot to fill.

Parrot rejection was one exact 8-gram window against the unredacted exemplars,
which a dump of the redacted text walks straight past. It now also fails on
prompt scaffolding in the output (bracketed placeholders, section headers,
separator lines) and on 5-gram coverage for drafts long enough to measure, and
it compares against the redacted exemplars the model actually saw.

Exemplars are clipped to 120 words (five retrieved pieces filled a 60 KB
prompt), and the prompt asks for a new post from the BRIEF with the EXAMPLES as
rhythm reference only. The holdout eval now disqualifies a guard-failing draft
from winning: a dump has near-perfect axis distance because it is the author's
own text, which is how it got crowned.

Co-authored-by: Cursor <cursoragent@cursor.com>
Redaction tidy-up was stripping every "'s" in the exemplar, not just the one
left behind by a cut name, so "That's it" reached the model as "That it" —
mangled rhythm in the very text the prompt asks it to imitate.

The holdout re-run's one RAG "win" was the mined bullets handed straight back:
no exemplar copied, so the parrot check passed, and a brief echo has excellent
axis distance. The brief now joins the copy-check pool alongside the exemplars.

Co-authored-by: Cursor <cursoragent@cursor.com>
Folding the brief into the parrot pool disqualified every draft in both arms:
an exact shared window with the brief is expected, because the brief is where
the content comes from. Brief echo is now its own looser check — near-total
coverage only — so it catches the bullets handed back without condemning a post
that legitimately uses them.

Co-authored-by: Cursor <cursoragent@cursor.com>
The rule banning bracketed placeholders spelled one out, putting the exact
string we do not want back in front of the model.

Co-authored-by: Cursor <cursoragent@cursor.com>
Normalize corpus text before embedding and storage so article-export CSS cannot enter prompts. Cap mined briefs at three short bullets and fail closed on excessive source overlap.

Co-authored-by: Cursor <cursoragent@cursor.com>
Route retrieval text through normalize_corpus_text so LinkedIn CSS/HTML
junk never enters exemplars. Cap mined briefs and reject high source
overlap so the eval cannot hand the model its own answer.

Co-authored-by: Cursor <cursoragent@cursor.com>
Voice reached the model only as five retrieved posts, and it answered by
continuing them: every RAG draft came back a multi-post dump while the
exemplar-free arm wrote clean posts. The style profile Lane C already
builds was never read by the write path.

Carry voice as measured cadence targets (sentence length, short-line
ratio, post length, pronoun lean, banned filler) and cut exemplars to two
short excerpts, so the signal survives with nothing to paste.

Brief mining cut sentences at a fixed word count, which manufactured
fragments ("IATA is now", "and one more time, please") that the writer
could only glue back together. Fit bullets to clause boundaries instead
and drop tails that leave a phrase mid-air.

Co-authored-by: Cursor <cursoragent@cursor.com>
The model wrote a strong opening then recycled its own lines until the
token budget ran out ("Say no. Say no. Say no."), so a good post lost to a
short one on axis distance. Lowering max_tokens only moves the cut
mid-sentence, so the tail is dropped after generation: paragraphs that
restate an earlier one go, then the draft stops at the last paragraph
boundary inside the measured word target.

Both arms get the same trim; applying a length edit to one side would
decide the comparison by itself.

Co-authored-by: Cursor <cursoragent@cursor.com>
The distance metric mixed raw pronoun counts with rates, so it measured
length instead of voice: a long draft paid a penalty per extra "you" while
a three-sentence stub sat at 0.08 from a 190-word post. Base "won" by not
writing a post.

Compare pronoun rates per 1k words and add a symmetric log-length term, so
a draft that is not the length of a post is penalized for it.

Enforce the same word ceiling the prompt states, rather than a looser one.

Co-authored-by: Cursor <cursoragent@cursor.com>
A lone median sentence length made the writer produce eighteen uniform
one-line sentences, which reads as a caricature of the voice and lost to a
draft with real paragraph structure. Measure the quartile spread and the
share of paragraphs carrying more than one sentence, and state both.

Co-authored-by: Cursor <cursoragent@cursor.com>
The corpus separates paragraphs with a single newline, so splitting only on
blank lines counted each stored post as one paragraph and put a false 100%
in the voice card.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Present the write path without internal experiment framing, failure
history, or "legacy because it failed" language. Advanced train/filter
commands stay as a short optional section.

Co-authored-by: Cursor <cursoragent@cursor.com>
@telivity-otaip
telivity-otaip merged commit 6919156 into main Jul 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant