Skip to content

Commit d997c63

Browse files
mpstatonclaude
andcommitted
milestone(fetch-metadata-preservation): fetch adds, never overwrites — Fixes #77
Fetch full content no longer clobbers the operator's metadata. Three layers: - fetchSourceContent (content-ingest) now reads the operator's saved title and bib from the source file and treats them as authoritative; Jina's values are demoted to fallbacks that fill only empty fields. The existing source_slug is preserved, so a re-fetch never renames the file. This also fixes the resolver's applyBibToRegistry path, which just echoes the preserved title back to the sources row. - The client merge keeps the operator's title/authors/publisher/date and takes only content + status from the fetch — and now surfaces "fetch failed — could not read the URL" instead of silently resetting the title to the URL (the old behavior that hid Jina failures). Verified: rsbuild build (strategy-curator) + tsc --noEmit (content-ingest) both green. Reaches augment.didi.sh on the next redeploy. The local-write toggle from the operator's desired pipeline is NOT in this commit — it's net-new behavior, tracked for a spec addendum. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UYTYu4MAFZ7iyr2VTo2kq
1 parent 29cd4fd commit d997c63

2 files changed

Lines changed: 25 additions & 9 deletions

File tree

apps/strategy-curator/src/curation.svelte.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,18 @@ class CurationState {
293293
this.saveStatus = this.lastError ?? 'fetch failed';
294294
return;
295295
}
296-
// merge so we keep fields the fetch result doesn't echo back (e.g. tags)
297-
this.replaceSource({ ...source, ...r.source });
298-
this.saveStatus = 'fetched';
296+
// Enrichment is additive: the fetch supplies content + status, but must NEVER
297+
// overwrite bibliographic fields the operator set. Keep the operator's title,
298+
// authors, publisher, and date; let the fetch fill only what was left empty.
299+
this.replaceSource({
300+
...source,
301+
...r.source,
302+
title: source.title || r.source.title,
303+
authors: source.authors?.length ? source.authors : r.source.authors,
304+
publisher: source.publisher || r.source.publisher,
305+
published_date: source.published_date || r.source.published_date,
306+
});
307+
this.saveStatus = r.source.content_pulled ? 'fetched' : 'fetch failed — could not read the URL';
299308
}
300309

301310
// retry — re-fetch with a Jina cache bypass (for stale/interstitial results)

services/content-ingest/src/corpus.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -665,10 +665,13 @@ export async function fetchSourceContent(
665665
args: FetchSourceArgs,
666666
): Promise<{ corpus_path: string; source_slug: string; title: string; content_pulled: boolean; via: string; binary_filename: string | null; publisher?: string; published_date?: string; authors?: string[] }> {
667667
const jr = await fetchViaJina(args.url, { noCache: args.no_cache });
668-
const title = jr.ok ? jr.title || args.url : args.url;
669668
const fullMd = jr.ok ? jr.markdown.trim() : '';
670669
const bib = jr.ok ? bibFromExtra(jr.extra) : {};
671-
const source_slug = args.source_slug || slugify(title) || slugify(args.url) || args.source_uuid.slice(0, 8);
670+
// Jina's title is only a FALLBACK — the operator's saved title wins (read below).
671+
const jinaTitle = jr.ok ? jr.title || args.url : args.url;
672+
// Stable existing slug wins so a re-fetch never renames the file; only a
673+
// brand-new source (no source_slug yet) derives its slug from the title.
674+
const source_slug = args.source_slug || slugify(jinaTitle) || slugify(args.url) || args.source_uuid.slice(0, 8);
672675
const dir = join(CLIENTS_ROOT, args.client_slug, 'corpus', domainFolder(args.domain_type), args.domain_slug, 'sources');
673676
await mkdir(dir, { recursive: true });
674677
const target = join(dir, `${source_slug}.md`);
@@ -687,12 +690,14 @@ export async function fetchSourceContent(
687690
// analyst may have hand-corrected, across the rewrite
688691
let extractsSection = EXTRACTS_SKELETON;
689692
let existingTags: string[] = [];
693+
let existingTitle: string | undefined;
690694
const existingBib: { publisher?: string; published_date?: string; authors?: string[] } = {};
691695
try {
692696
const existing = await readFile(target, 'utf8');
693697
const idx = existing.indexOf('# Extracts');
694698
if (idx >= 0) extractsSection = existing.slice(idx);
695699
existingTags = parseTagsFromFrontmatter(existing);
700+
existingTitle = parseFmScalar(existing, 'title');
696701
existingBib.publisher = parseFmScalar(existing, 'publisher');
697702
existingBib.published_date = parseFmScalar(existing, 'published_date');
698703
const ea = parseListFromFrontmatter(existing, 'authors');
@@ -701,11 +706,13 @@ export async function fetchSourceContent(
701706
// fresh — use the skeleton
702707
}
703708

704-
// Jina's fresh metadata wins; fall back to whatever was already on the file.
709+
// The operator's saved metadata is authoritative — enrichment is ADDITIVE:
710+
// Jina only FILLS fields the operator left empty, and never overwrites them.
711+
const title = existingTitle?.trim() || jinaTitle;
705712
const merged = {
706-
publisher: bib.publisher ?? existingBib.publisher,
707-
published_date: bib.published_date ?? existingBib.published_date,
708-
authors: bib.authors ?? existingBib.authors,
713+
publisher: existingBib.publisher ?? bib.publisher,
714+
published_date: existingBib.published_date ?? bib.published_date,
715+
authors: existingBib.authors?.length ? existingBib.authors : bib.authors,
709716
};
710717

711718
const fm = buildSourceFrontmatter({

0 commit comments

Comments
 (0)