Skip to Content
PlansParser Hardening — Implementation Plan

Parser Hardening — Implementation Plan

Source: plans/step-5.1.2-cv-parser-hardening-impl.md · synced 2026-06-12

# Parser Hardening — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use executing-plans (tasks are sequential, not parallel-able). Steps use checkbox (- [ ]) syntax for tracking. Each task ends with a commit.

Status (2026-05-25): ✅ Shipped on feat/parser-hardening branch. Phase A, 9-step coverage: Step 5a hardening.

Depends on: Approved design in plans/step-5.1.2-cv-parser-hardening-design.md

Goal: Add deterministic source-grounding and stale-CV detection to the CV parser so hallucinations and out-of-date CVs are caught before they flow into the title normaliser.

Architecture: Two new modules (grounding.ts, staleness.ts) run as post-checks inside parseCVText. Schema extends ParseMetadata with ungrounded_items, stale_cv, last_role_end. Backwards compatible — the old 66 JSONs get overwritten when batch-parse re-runs.

Tech Stack: No new deps. TypeScript + Vitest.


File Map

FileChange
src/lib/cv-parser/types.tsExtend ParseMetadata; add UngroundedItem
src/lib/cv-parser/grounding.tsNEW — checkGrounding(cv, sourceText)
src/lib/cv-parser/staleness.tsNEW — checkStaleness(cv, now?)
src/lib/cv-parser/parser.tsWire in both checks; tighten prompt; update failedParseRecord and deriveParseStatus
src/lib/cv-parser/index.tsPass extraction.text through to parseCVText
tests/lib/cv-parser/grounding.test.tsNEW — unit tests
tests/lib/cv-parser/staleness.test.tsNEW — unit tests
context/current-data.mdUpdate — workspace doc currently misstates the parser/CV state

Task 1: Extend ParseMetadata types

Files:

  • Modify: calibre-app/src/lib/cv-parser/types.ts

  • Step 1: Add the new types

Append to calibre-app/src/lib/cv-parser/types.ts:

export interface UngroundedItem { field: 'commodity_specialisations' | 'certifications' | 'tools_and_systems' item: string }

Modify the existing ParseMetadata interface to add three required fields:

export interface ParseMetadata { source_file: string file_type: FileType parse_status: ParseStatus unreadable_reason: string | null overall_confidence: number parsed_at: string ungrounded_items: UngroundedItem[] stale_cv: boolean last_role_end: string | null }
  • Step 2: Type-check
cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && npx tsc --noEmit

Expected: errors in parser.ts and tests because parse_metadata no longer matches. Leave them — they’ll be fixed in later tasks.

  • Step 3: Commit (just the type changes — break is intentional and will be resolved by end of plan)
cd "G:/My Drive/BL Dev work/Calibre/calibre-app" git add src/lib/cv-parser/types.ts git commit -m "feat(types): extend ParseMetadata with grounding + staleness fields"

Task 2: Grounding module (TDD)

Files:

  • Create: calibre-app/src/lib/cv-parser/grounding.ts

  • Create: calibre-app/tests/lib/cv-parser/grounding.test.ts

  • Step 1: Write the failing tests

Create calibre-app/tests/lib/cv-parser/grounding.test.ts:

import { describe, it, expect } from 'vitest' import { isGrounded, checkGrounding } from '@/lib/cv-parser/grounding' import type { ParsedCV } from '@/lib/cv-parser/types' describe('isGrounded', () => { it('returns true when all significant tokens appear in source', () => { expect(isGrounded('Crude Oil', 'I worked on crude oil trading.')).toBe(true) }) it('returns false when no significant token appears in source', () => { expect(isGrounded('LNG', 'I worked on fuel oil and bunkers.')).toBe(false) }) it('matches short acronyms as whole words only', () => { expect(isGrounded('LNG', 'Spot LNG trading desk.')).toBe(true) // "LNG" embedded inside "LONGEVITY" should NOT count expect(isGrounded('LNG', 'Longevity studies and longitudinal data.')).toBe(false) }) it('is case-insensitive for multi-char tokens', () => { expect(isGrounded('Bloomberg', 'Built dashboards in BLOOMBERG.')).toBe(true) }) it('returns false if any required token is missing', () => { // "Crack Spread Products" — significant tokens: crack, spread, products // Source only mentions "crack" → ungrounded expect(isGrounded('Crack Spread Products', 'Traded crack and other refined products.')).toBe(false) }) it('ignores stop words', () => { expect(isGrounded('Risk and Controls', 'Built the risk controls framework.')).toBe(true) }) it('returns false for empty item', () => { expect(isGrounded('', 'anything')).toBe(false) }) }) const fixture = (): ParsedCV => ({ identity: { name: { value: 'Test', confidence: 1 }, email: { value: null, confidence: null }, phone: { value: null, confidence: null }, location: { value: null, confidence: null }, linkedin_url: { value: null, confidence: null }, }, current_role: { raw_title: { value: 'Trader', confidence: 1 }, employer: { value: 'Acme', confidence: 1 }, start_date: { value: '2020-01', confidence: 1 }, end_date: { value: null, confidence: null }, }, employment_history: [], signals: { total_years_experience: { value: 5, confidence: 1 }, seniority_signal: { value: 'IC', confidence: 1 }, commodity_specialisations: { value: ['Crude Oil', 'LNG'], confidence: 0.95 }, vertical_signals: { value: [], confidence: 1 }, tools_and_systems: { value: ['Bloomberg'], confidence: 1 }, certifications: { value: [], confidence: 1 }, education: [], career_gaps: { value: [], confidence: 1 }, short_tenures: { value: [], confidence: 1 }, }, parse_metadata: { source_file: 'test.pdf', file_type: 'pdf', parse_status: 'success', unreadable_reason: null, overall_confidence: 0.95, parsed_at: '2026-05-20T00:00:00Z', ungrounded_items: [], stale_cv: false, last_role_end: null, }, }) describe('checkGrounding', () => { it('clamps confidence to 0.5 when one item is ungrounded', () => { const cv = fixture() const source = 'Crude oil trader in Singapore using Bloomberg.' const result = checkGrounding(cv, source) expect(result.clamped.signals.commodity_specialisations.confidence).toBeLessThanOrEqual(0.5) expect(result.ungroundedItems).toContainEqual({ field: 'commodity_specialisations', item: 'LNG' }) }) it('leaves confidence untouched when all items ground', () => { const cv = fixture() const source = 'Crude oil and LNG trader. Bloomberg user.' const result = checkGrounding(cv, source) expect(result.clamped.signals.commodity_specialisations.confidence).toBe(0.95) expect(result.ungroundedItems).toEqual([]) }) it('keeps ungrounded items in the value array (does not delete)', () => { const cv = fixture() const source = 'Crude oil trader using Bloomberg.' const result = checkGrounding(cv, source) expect(result.clamped.signals.commodity_specialisations.value).toEqual(['Crude Oil', 'LNG']) }) })
  • Step 2: Run tests to verify they fail
cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && npm test tests/lib/cv-parser/grounding.test.ts

Expected: FAIL — module not found.

  • Step 3: Write the grounding module

Create calibre-app/src/lib/cv-parser/grounding.ts:

import type { ParsedCV, UngroundedItem } from './types' const STOP_WORDS = new Set([ 'the', 'and', 'for', 'with', 'from', 'into', 'product', 'products', 'service', 'services', 'system', 'systems', 'group', 'team', ]) interface TokenSet { longTokens: string[] acronyms: string[] } function tokenise(item: string): TokenSet { const cleaned = item.replace(/[^a-zA-Z0-9\s]/g, ' ') const words = cleaned.split(/\s+/).filter(Boolean) const longTokens: string[] = [] const acronyms: string[] = [] for (const w of words) { const lower = w.toLowerCase() if (STOP_WORDS.has(lower)) continue if (w.length >= 4) longTokens.push(lower) else if (w.length >= 2) acronyms.push(w) } return { longTokens, acronyms } } export function isGrounded(item: string, sourceText: string): boolean { const { longTokens, acronyms } = tokenise(item) if (longTokens.length === 0 && acronyms.length === 0) return false const sourceLower = sourceText.toLowerCase() for (const t of longTokens) { if (!sourceLower.includes(t)) return false } for (const a of acronyms) { const re = new RegExp(`\\b${a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i') if (!re.test(sourceText)) return false } return true } export interface GroundingResult { clamped: ParsedCV ungroundedItems: UngroundedItem[] } const GROUNDED_FIELDS: Array<UngroundedItem['field']> = [ 'commodity_specialisations', 'certifications', 'tools_and_systems', ] export function checkGrounding(cv: ParsedCV, sourceText: string): GroundingResult { const ungrounded: UngroundedItem[] = [] for (const field of GROUNDED_FIELDS) { const ref = cv.signals[field] let anyUngrounded = false for (const item of ref.value) { if (!isGrounded(item, sourceText)) { ungrounded.push({ field, item }) anyUngrounded = true } } if (anyUngrounded) { const current = ref.confidence ?? 0 ref.confidence = Math.min(current, 0.5) } } return { clamped: cv, ungroundedItems: ungrounded } }
  • Step 4: Run tests to verify they pass
cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && npm test tests/lib/cv-parser/grounding.test.ts

Expected: PASS — all grounding tests green.

  • Step 5: Commit
git add src/lib/cv-parser/grounding.ts tests/lib/cv-parser/grounding.test.ts git commit -m "feat(parser): add source-grounding check for list fields"

Task 3: Staleness module (TDD)

Files:

  • Create: calibre-app/src/lib/cv-parser/staleness.ts

  • Create: calibre-app/tests/lib/cv-parser/staleness.test.ts

  • Step 1: Write the failing tests

Create calibre-app/tests/lib/cv-parser/staleness.test.ts:

import { describe, it, expect } from 'vitest' import { checkStaleness } from '@/lib/cv-parser/staleness' import type { ParsedCV } from '@/lib/cv-parser/types' const baseFixture = (): ParsedCV => ({ identity: { name: { value: 'Test', confidence: 1 }, email: { value: null, confidence: null }, phone: { value: null, confidence: null }, location: { value: null, confidence: null }, linkedin_url: { value: null, confidence: null }, }, current_role: { raw_title: { value: 'Trader', confidence: 1 }, employer: { value: 'Acme', confidence: 1 }, start_date: { value: '2020-01', confidence: 1 }, end_date: { value: null, confidence: null }, }, employment_history: [], signals: { total_years_experience: { value: 5, confidence: 1 }, seniority_signal: { value: 'IC', confidence: 1 }, commodity_specialisations: { value: [], confidence: 1 }, vertical_signals: { value: [], confidence: 1 }, tools_and_systems: { value: [], confidence: 1 }, certifications: { value: [], confidence: 1 }, education: [], career_gaps: { value: [], confidence: 1 }, short_tenures: { value: [], confidence: 1 }, }, parse_metadata: { source_file: 'test.pdf', file_type: 'pdf', parse_status: 'success', unreadable_reason: null, overall_confidence: 0.95, parsed_at: '2026-05-20T00:00:00Z', ungrounded_items: [], stale_cv: false, last_role_end: null, }, }) const NOW = new Date(2026, 4, 20) // 2026-05-20 describe('checkStaleness', () => { it('returns not-stale when current_role end_date is null', () => { const cv = baseFixture() const result = checkStaleness(cv, NOW) expect(result.staleCV).toBe(false) expect(result.lastRoleEnd).toBeNull() }) it('returns not-stale when any history role has null end_date', () => { const cv = baseFixture() cv.current_role.end_date = { value: '2020-12', confidence: 1 } cv.employment_history = [ { raw_title: { value: 'X', confidence: 1 }, employer: { value: 'Y', confidence: 1 }, start_date: { value: '2021-01', confidence: 1 }, end_date: { value: null, confidence: null }, description_summary: { value: '', confidence: 1 } }, ] const result = checkStaleness(cv, NOW) expect(result.staleCV).toBe(false) }) it('flags stale when latest end_date is over 12 months ago (Jonathan Lam case)', () => { const cv = baseFixture() cv.current_role.end_date = { value: '2019-01', confidence: 1 } cv.employment_history = [ { raw_title: { value: 'X', confidence: 1 }, employer: { value: 'Y', confidence: 1 }, start_date: { value: '2016-03', confidence: 1 }, end_date: { value: '2016-11', confidence: 1 }, description_summary: { value: '', confidence: 1 } }, ] const result = checkStaleness(cv, NOW) expect(result.staleCV).toBe(true) expect(result.lastRoleEnd).toBe('2019-01') }) it('does NOT flag stale when latest end_date is within 12 months', () => { const cv = baseFixture() cv.current_role.end_date = { value: '2025-08', confidence: 1 } // 9 months ago from NOW const result = checkStaleness(cv, NOW) expect(result.staleCV).toBe(false) expect(result.lastRoleEnd).toBe('2025-08') }) it('returns null lastRoleEnd when no parseable dates', () => { const cv = baseFixture() cv.current_role.end_date = { value: 'unknown', confidence: 0.1 } const result = checkStaleness(cv, NOW) expect(result.staleCV).toBe(false) expect(result.lastRoleEnd).toBeNull() }) })
  • Step 2: Run tests to verify they fail
cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && npm test tests/lib/cv-parser/staleness.test.ts

Expected: FAIL — module not found.

  • Step 3: Write the staleness module

Create calibre-app/src/lib/cv-parser/staleness.ts:

import type { ParsedCV } from './types' const STALE_THRESHOLD_MONTHS = 12 function parseYearMonth(ym: string | null): Date | null { if (!ym) return null const m = ym.match(/^(\d{4})-(\d{2})$/) if (!m) return null const year = parseInt(m[1], 10) const month = parseInt(m[2], 10) if (month < 1 || month > 12) return null return new Date(year, month - 1, 1) } function formatYearMonth(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}` } export interface StalenessResult { staleCV: boolean lastRoleEnd: string | null } export function checkStaleness(cv: ParsedCV, now: Date = new Date()): StalenessResult { if (cv.current_role.end_date.value === null) { return { staleCV: false, lastRoleEnd: null } } for (const role of cv.employment_history) { if (role.end_date.value === null) { return { staleCV: false, lastRoleEnd: null } } } const dates: Date[] = [] const currentEnd = parseYearMonth(cv.current_role.end_date.value) if (currentEnd) dates.push(currentEnd) for (const role of cv.employment_history) { const d = parseYearMonth(role.end_date.value) if (d) dates.push(d) } if (dates.length === 0) { return { staleCV: false, lastRoleEnd: null } } const latest = dates.reduce((a, b) => (a > b ? a : b)) const cutoff = new Date(now.getFullYear(), now.getMonth() - STALE_THRESHOLD_MONTHS, 1) const stale = latest < cutoff return { staleCV: stale, lastRoleEnd: formatYearMonth(latest) } }
  • Step 4: Run tests to verify they pass
cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && npm test tests/lib/cv-parser/staleness.test.ts

Expected: PASS.

  • Step 5: Commit
git add src/lib/cv-parser/staleness.ts tests/lib/cv-parser/staleness.test.ts git commit -m "feat(parser): add stale-CV detection"

Task 4: Integrate checks + tighten prompt

Files:

  • Modify: calibre-app/src/lib/cv-parser/parser.ts

  • Step 1: Update imports + prompt

In calibre-app/src/lib/cv-parser/parser.ts, add these imports near the top:

import { checkGrounding } from './grounding' import { checkStaleness } from './staleness'

Update the system/user prompt in parseCVText to add this bullet (after the existing seniority_signal bullet, before the career_gaps bullet):

- Do NOT split a continuous tenure at one employer into multiple rows. If a candidate held multiple titles at the same employer with no explicit role-change dates in the text, use the latest title and describe role progression in description_summary.
  • Step 2: Update parseCVText to call the checks

Inside parseCVText, after the existing const extracted = toolUse.input as Omit<ParsedCV, 'parse_metadata'> line, replace the metadata block with:

const cvWithEmptyMeta: ParsedCV = { ...(extracted as Omit<ParsedCV, 'parse_metadata'>), parse_metadata: { source_file: sourceFile, file_type: ext, parse_status: 'success', unreadable_reason: null, overall_confidence: 0, parsed_at: new Date().toISOString(), ungrounded_items: [], stale_cv: false, last_role_end: null, }, } const grounding = checkGrounding(cvWithEmptyMeta, rawText) const staleness = checkStaleness(grounding.clamped) const overallConfidence = computeOverallConfidence(grounding.clamped) const parseStatus = deriveParseStatus( grounding.clamped, overallConfidence, grounding.ungroundedItems.length, ) return { ...grounding.clamped, parse_metadata: { ...grounding.clamped.parse_metadata, parse_status: parseStatus, overall_confidence: overallConfidence, parsed_at: new Date().toISOString(), ungrounded_items: grounding.ungroundedItems, stale_cv: staleness.staleCV, last_role_end: staleness.lastRoleEnd, }, }
  • Step 3: Update deriveParseStatus to accept ungroundedCount

Replace the existing deriveParseStatus function with:

function deriveParseStatus( cv: Omit<ParsedCV, 'parse_metadata'>, overallConfidence: number, ungroundedCount: number, ): ParseStatus { const requiredFields: Array<ConfidenceField<unknown>> = [ cv.identity.name, cv.current_role.raw_title, cv.current_role.employer, cv.signals.total_years_experience, ] const hasLowConfidence = requiredFields.some( f => f.confidence === null || f.confidence < 0.60 ) if (ungroundedCount > 0) return 'partial' if (hasLowConfidence || overallConfidence < 0.70) return 'partial' return 'success' }
  • Step 4: Update failedParseRecord to include new metadata fields

In failedParseRecord, extend the parse_metadata object with:

ungrounded_items: [], stale_cv: false, last_role_end: null,
  • Step 5: Run all parser tests
cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && npm test

Expected: PASS — all 12 existing tests plus the new grounding + staleness tests still pass. Parser tests make real API calls, so this takes ~100s.

  • Step 6: Commit
git add src/lib/cv-parser/parser.ts git commit -m "feat(parser): wire in grounding + staleness checks; tighten row-split prompt"

Task 5: Public API plumbing

Files:

  • Modify: calibre-app/src/lib/cv-parser/index.ts

The grounding check needs the raw text. Currently parseCVText is called with extraction.text already — verify and adjust if needed.

  • Step 1: Verify index.ts passes rawText

Read calibre-app/src/lib/cv-parser/index.ts. The line should already be:

return parseCVText(extraction.text, sourceFile)

If yes, no change needed.

  • Step 2: Type-check
cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && npx tsc --noEmit

Expected: zero errors.

  • Step 3: Run full test suite
cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && npm test

Expected: all tests pass.

  • Step 4: Commit (if any change was needed; otherwise skip)
git add src/lib/cv-parser/index.ts git commit -m "chore(parser): confirm rawText flows into hardened parser"

Task 6: Re-batch parse + verify hardening

Files:

  • No code changes. Re-run batch parse against the 66 reference CVs.

  • Step 1: Re-run batch parse

cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && npm run batch-parse

Expected output: status distribution shifts. partial count rises from 0 because ungrounded items now downgrade status. failed stays at 1 (Chloe Wong).

  • Step 2: Verify the 4 known issues are now caught

Run a quick inspection script:

cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && node -e " const fs=require('fs'); const cases=[ ['Adrian Tan (Risk, Oil).json', 'LNG hallucination'], ['Jonathan Lam (Middle Office, Oil).json','stale CV + Crude Oil hallucination'], ['Anand Upretic (Risk, Oil).json', 'commodity completeness'], ['Sim Qi Xuan (Cargo Ops, Fuel Oil).json','should remain partial/honest'], ]; for(const [f,note] of cases){ const j=JSON.parse(fs.readFileSync('outputs/parsed-cvs/'+f,'utf8')); console.log('---',f,'---'); console.log('note: ',note); console.log('parse_status: ',j.parse_metadata.parse_status); console.log('overall_confidence: ',j.parse_metadata.overall_confidence); console.log('stale_cv: ',j.parse_metadata.stale_cv); console.log('last_role_end: ',j.parse_metadata.last_role_end); console.log('ungrounded_items: ',JSON.stringify(j.parse_metadata.ungrounded_items)); console.log('commodity confidence: ',j.signals.commodity_specialisations.confidence); } "

Acceptance criteria:

  • Adrian Tan → ungrounded_items contains {field: "commodity_specialisations", item: "LNG"}, commodity_specialisations.confidence ≤ 0.5, parse_status: "partial".

  • Jonathan Lam → stale_cv: true, last_role_end: "2019-01", parse_status: "partial".

  • Sim Qi Xuan → still parses cleanly (or partial if any new flags fire — that’s fine).

  • Overall failed count = 1 (Chloe Wong only).

  • Step 3: Aggregate status distribution

cd "G:/My Drive/BL Dev work/Calibre/calibre-app" && node -e " const fs=require('fs'); const files=fs.readdirSync('outputs/parsed-cvs').filter(f=>f.endsWith('.json')); const counts={success:0,partial:0,failed:0}; const ungroundedHits=[], staleHits=[]; for(const f of files){ const j=JSON.parse(fs.readFileSync('outputs/parsed-cvs/'+f,'utf8')); counts[j.parse_metadata.parse_status]++; if(j.parse_metadata.ungrounded_items.length>0) ungroundedHits.push(f); if(j.parse_metadata.stale_cv) staleHits.push(f); } console.log('Status:',counts); console.log('CVs with ungrounded items:',ungroundedHits.length); console.log('Stale CVs:',staleHits.length); console.log('Stale list:',staleHits); "

Record the numbers — they go into the project state doc in Task 7.

  • Step 4: Commit the re-batched JSONs
git add outputs/parsed-cvs/ git commit -m "chore(parsed-cvs): re-batch with hardened parser"

Task 7: Update workspace state doc

Files:

  • Modify: context/current-data.md

  • Step 1: Update the CV-count line and unreadable-files list

In context/current-data.md:

  • Change “65 real candidate CVs” to “66 real candidate CVs”.

  • Replace the 4-item “Unreadable files” list with a single bullet: Chloe Wong (Market Risk, Metals).pdf — scanned image PDF. Note that Sandeep Singh, Anand Upretic, and Jonathan Lam all parse cleanly (previously misclassified).

  • Step 2: Append a calibration-audit section

Add a new section after “Parser Schema Design Decisions (2026-05-19)”:

## Parser Calibration Audit (2026-05-20) A 7-CV audit compared parsed JSON against source CVs: - **Floor honesty:** scores ≤0.90 are calibrated — model genuinely hedges. - **Top is anchored:** scores ≥0.95 frequently mask hallucinations (e.g. Adrian Tan flagged with "LNG" specialisation that appears nowhere in his CV). - **Hallucinations cluster in list fields:** `commodity_specialisations`, `certifications`, `tools_and_systems`. - **Stale CVs treated as current:** Jonathan Lam's CV ended Jan 2019 but parser called the SOCAR role `current_role` with confidence 1.0. **Hardening applied (2026-05-20):** - Deterministic source-grounding check on list fields → ungrounded items keep field confidence ≤ 0.5 and land in `parse_metadata.ungrounded_items`. - Stale-CV detection at 12-month threshold → `parse_metadata.stale_cv` + `last_role_end`. - Prompt change to discourage row-fabrication at the same employer. After hardening, batch re-run results (fill in after Task 6 runs): - Status: { success: TBD, partial: TBD, failed: 1 } - CVs flagged with ungrounded items: TBD - Stale CVs: TBD

(Replace the TBD lines with the actual numbers from Task 6 Step 3.)

  • Step 3: Commit the doc update
cd "G:/My Drive/BL Dev work/Calibre" && git -C calibre-app status # confirm no uncommitted code # the workspace root is not a git repo; the doc edit is just a file save

No commit needed at workspace root — context/current-data.md lives outside the calibre-app git tree.


Self-Review Checklist

  • Spec coverage: Grounding ✓, Staleness ✓, Prompt tightening ✓, Metadata extension ✓, Re-batch ✓, Workspace doc ✓
  • TDD discipline: Each new module has a failing test before implementation
  • Backwards compatible at the JSON-output level: new fields are added, existing fields unchanged. Old consumers ignore the new fields harmlessly.
  • Acceptance criteria: Task 6 Step 2 names specific JSONs that must change in specific ways. Easy to verify.

What This Plan Does NOT Cover

  • Row-fabrication detection pass (Rachel Seah) — deferred. Watch the Task 6 output. If Concordia still splits into 3 rows after the prompt change, raise a follow-up plan.
  • Title normaliser — next sprint task.
  • Supabase storage — infrastructure setup.
  • UI for human review of flagged items — Sprint 4.
Last updated on