diff --git a/SESSION.md b/SESSION.md index 04c1519..b7a78b3 100644 --- a/SESSION.md +++ b/SESSION.md @@ -1,26 +1,38 @@ ## AI tools used +- ChatGPT/Codex in the local repo workspace. ## Prompts and instructions -List the prompts or instructions you gave to AI tools during the challenge. +- Reviewed the current citation rendering flow: how `answer.evidence` is resolved, where inline markers render, how source metadata is attached, and where visibility checks happen. +- Asked for the smallest clean fix in `lib/citations.ts` only, without changing React components or redesigning the data model. +- Asked to verify assumptions against the actual TypeScript types and fixtures. +- Ran the full challenge validation with `npm test` and `npm run build`. ## AI outputs accepted -Summarize the AI outputs, edits, commands, or recommendations you accepted. +- Updated `resolveCitations` to use the viewer context before adding citations. +- Counted unavailable evidence when evidence/source lookup fails or the viewer cannot use the evidence. +- Kept citation grouping and numbering based on the first visible evidence item in `answer.evidence` order. +- Applied evidence redactions before excerpts are returned to the UI. +- Kept all UI components unchanged. ## Manual work -Summarize the parts you wrote, changed, reviewed, or corrected manually. +- Reviewed the changed resolver against `data/types.ts` and `data/fixtures.ts`. +- Verified that `Answer.asOf`, `Source.requiredGroups`, `EvidenceChunk.validFrom`, optional `validUntil`, optional `redactions`, and the redaction field names matched the implementation. +- Ran `npm test`, `npm run typecheck`, and `npm run build`. ## Discarded AI output -Summarize any AI suggestions you intentionally did not use and why. +- Did not move citation safety logic into React components. +- Did not redesign the data model or add new behavior beyond the citation safety fix. +- Did not add extra tests because the existing citation tests covered the requested behavior. ## Notes -Do not include secrets, access tokens, private credentials, or personal data. Redact anything sensitive before submission. +- The first focused test attempt failed because dependencies were not installed yet. After dependencies were available, tests and build passed. diff --git a/SUBMISSION.md b/SUBMISSION.md index 18d4dde..cdac929 100644 --- a/SUBMISSION.md +++ b/SUBMISSION.md @@ -1,12 +1,17 @@ ## What changed? +Fixed citation resolution in `lib/citations.ts` so only evidence the current viewer can use is rendered. The resolver now checks source/evidence visibility before grouping citations, counts unavailable evidence references, only assigns inline citation numbers after checks pass, and redacts restricted excerpt text before it reaches the UI. ## What did you test? +- `npm test` +- `npm run build` ## Session log -- [ ] `SESSION.md` is completed. +- [x] `SESSION.md` is completed. ## Final commit SHA + +3b242ae3fb6840768a72d7cb085313d674dc504c diff --git a/lib/citations.ts b/lib/citations.ts index 3976e9e..e126492 100644 --- a/lib/citations.ts +++ b/lib/citations.ts @@ -33,7 +33,17 @@ export function canViewSource(source: Source, context: ViewerContext): boolean { } if (source.visibility === "internal") { - return context.role === "admin" && context.isInternalEmployee; + if (context.role !== "admin" || !context.isInternalEmployee) { + return false; + } + } + + if (source.region !== "global" && source.region !== context.region) { + return false; + } + + if (!source.requiredGroups.every((group) => context.groups.includes(group))) { + return false; } return true; @@ -42,36 +52,53 @@ export function canViewSource(source: Source, context: ViewerContext): boolean { export function canUseEvidence( evidence: EvidenceChunk, source: Source, - context: ViewerContext + context: ViewerContext, + asOf: string ): boolean { if (evidence.status !== "verified") { return false; } + if (evidence.validFrom > asOf) { + return false; + } + + if (evidence.validUntil && evidence.validUntil < asOf) { + return false; + } + return canViewSource(source, context); } +function redactExcerpt(excerpt: string, evidence: EvidenceChunk, context: ViewerContext): string { + return (evidence.redactions ?? []).reduce((safeExcerpt, redaction) => { + if (redaction.visibleToGroups.some((group) => context.groups.includes(group))) { + return safeExcerpt; + } + + return safeExcerpt.replaceAll(redaction.text, redaction.replacement); + }, excerpt); +} + export function resolveCitations( answer: Answer, evidenceChunks: EvidenceChunk[], allSources: Source[], context: ViewerContext ): CitationResolution { - void context; - const evidenceById = new Map(evidenceChunks.map((evidence) => [evidence.id, evidence])); const sourcesById = new Map(allSources.map((source) => [source.id, source])); const citationBySourceId = new Map(); const citations: ResolvedCitation[] = []; const evidenceToCitationNumber: Record = {}; + let unavailableCount = 0; - // BUG: the legacy resolver trusts retrieval output and groups every matching - // evidence ID. It misses source policy, time windows, redactions, and unavailable counts. for (const reference of answer.evidence) { const evidence = evidenceById.get(reference.evidenceId); const source = evidence ? sourcesById.get(evidence.sourceId) : undefined; - if (!evidence || !source) { + if (!evidence || !source || !canUseEvidence(evidence, source, context, answer.asOf)) { + unavailableCount += 1; continue; } @@ -93,12 +120,12 @@ export function resolveCitations( citation.evidence.push({ id: evidence.id, - excerpt: evidence.excerpt, + excerpt: redactExcerpt(evidence.excerpt, evidence, context), updatedAt: evidence.updatedAt, confidence: reference.confidence }); evidenceToCitationNumber[evidence.id] = citation.citationNumber; } - return { citations, unavailableCount: 0, evidenceToCitationNumber }; + return { citations, unavailableCount, evidenceToCitationNumber }; }