feat(pipeline): feed the current source page into the extraction prompt

#103 made ingest merge into the page already on disk instead of
clobbering it, but the extraction itself stayed page-blind: the model
wrote every page as if fresh, and Merge reconciled the two texts
afterwards by heading. It could restate a claim but never refine it,
and had no way to know what already existed.

BuildPrompt now accepts the source page's current on-disk content
(when one exists) and asks the model to revise it explicitly: add
what's missing, strengthen what the source now supports, leave the
rest alone and reuse its wording verbatim so unchanged claims are
recognized as unchanged. Scoped to the source page specifically —
its path (wiki/sources/<sourceSlug>.md) is deterministic before
extraction runs, unlike a concept/entity page's path, which only
exists once the model has produced a title.

The deterministic merge in wiki.Merge is unchanged and stays the
backstop: an on-disk section the model doesn't touch survives
untouched regardless of what the prompt asks for, so a model that
ignores the instruction still cannot destroy human content.

Closes hyperguild#106 (first half; heading normalization follows).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 07:38:07 +02:00
co-authored by Claude Sonnet 5
parent cdafdf6e0f
commit 6e83303752
3 changed files with 141 additions and 8 deletions
+24 -2
View File
@@ -22,8 +22,18 @@ Each element must have exactly these fields:
Wikilinks in content: [[Display Name]] — just the display name, no slug, no pipe separator.
Only link to pages listed in the inventory or pages you are creating in this response.`
// BuildPrompt constructs the user prompt for a single chunk.
func BuildPrompt(schema, source, content string, inventory map[wiki.PageType][]wiki.Entry) string {
// BuildPrompt constructs the user prompt for a single chunk. existingPage is
// the full current content (frontmatter + body) of the source's own wiki page
// when one already exists on disk, or "" for a source being ingested for the
// first time. When non-empty, the model is asked to revise that page rather
// than write a fresh one — see hyperguild#106. The deterministic merge in
// wiki.Merge remains the backstop regardless of what the model returns: an
// on-disk section the model doesn't touch survives untouched, and a section
// it does touch only takes effect where Merge's strategy allows the fresh
// side to change anything (bullet union, append) — never for an ordinary
// section, so the model proposing a revision is never how a page gets
// overwritten.
func BuildPrompt(schema, source, content string, inventory map[wiki.PageType][]wiki.Entry, existingPage string) string {
var sb strings.Builder
fmt.Fprintf(&sb, "Today's date is %s.\n\n", time.Now().UTC().Format("2006-01-02"))
@@ -49,6 +59,18 @@ func BuildPrompt(schema, source, content string, inventory map[wiki.PageType][]w
sb.WriteString("\n")
}
if existingPage != "" {
sb.WriteString("## Current page for this source — revise it, don't restart\n\n")
sb.WriteString("This source has already been ingested and has the wiki page shown below.\n")
sb.WriteString("You are revising that page, not writing a fresh one:\n")
sb.WriteString(" - Add sections or claims the source below supports that this page doesn't have yet.\n")
sb.WriteString(" - Strengthen a claim where the source gives more detail, evidence, or nuance than what's already there.\n")
sb.WriteString(" - Leave unchanged whatever the source doesn't affect — reuse the existing wording verbatim rather than paraphrasing it, so an unchanged claim is recognized as unchanged.\n")
sb.WriteString(" - A merge step keeps this page's on-disk content for any section you don't return, so it's fine to omit a section you aren't revising.\n\n")
sb.WriteString(existingPage)
sb.WriteString("\n\n")
}
sb.WriteString("## Non-negotiable rules\n\n")
sb.WriteString("1. Output ONLY a valid JSON array — no prose, no fences.\n")
sb.WriteString("2. Fields: title, type, subtype (if applicable), domain (if applicable), content.\n")