Merge branch 'main' of ssh://ramanujan.glitch.university:2222/glitch-university/gupi-osint-board
This commit is contained in:
@@ -293,6 +293,18 @@ suite('normalized level persistence API', () => {
|
|||||||
const fatherJudgment = await (await adminFetch(`${baseUrl}/api/levels/${imported.playableLevel.id}/documents/${fatherDocument.id}/judge`, { method:'POST' })).json()
|
const fatherJudgment = await (await adminFetch(`${baseUrl}/api/levels/${imported.playableLevel.id}/documents/${fatherDocument.id}/judge`, { method:'POST' })).json()
|
||||||
expect(fatherJudgment).toMatchObject({ status:'succeeded',subject:'related',awardedFlags:['scene7.father_inventor_discovered'],
|
expect(fatherJudgment).toMatchObject({ status:'succeeded',subject:'related',awardedFlags:['scene7.father_inventor_discovered'],
|
||||||
goals:[expect.objectContaining({ key:'barricelli.inventor-proof',status:'pending' })] })
|
goals:[expect.objectContaining({ key:'barricelli.inventor-proof',status:'pending' })] })
|
||||||
|
const relatedState=await (await fetch(`${baseUrl}/api/levels/${imported.playableLevel.id}`)).json() as CaseState
|
||||||
|
const relatedClaim=relatedState.exhibits.find(exhibit => exhibit.type === 'claim')!
|
||||||
|
const relatedConnectionId=randomUUID()
|
||||||
|
relatedState.connections.push({ id:relatedConnectionId,fromExhibitId:relatedClaim.id,toExhibitId:fatherDocument.id,label:'Proof that the Barricelli family included an inventor.',tightness:65,tagStyle:'luggage',tagPosition:50,tagOffset:0 })
|
||||||
|
expect((await fetch(`${baseUrl}/api/levels/${relatedState.id}`,{ method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify(relatedState) })).status).toBe(200)
|
||||||
|
const relatedSubmission=await (await fetch(`${baseUrl}/api/levels/${relatedState.id}/report/submissions`,{ method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({
|
||||||
|
investigatorName:'Test Player',evidence:[{ connectionId:relatedConnectionId,documentExhibitId:fatherDocument.id,relationText:'Proof that the Barricelli family included an inventor.' }],
|
||||||
|
}) })).json()
|
||||||
|
expect(relatedSubmission).toMatchObject({ status:'evidence_insufficient',issues:expect.arrayContaining(['missing_accepted_evidence','connected_evidence_unverified']),
|
||||||
|
feedback:expect.stringContaining('related person rather than the claim subject'),claims:[expect.objectContaining({ evidence:[expect.objectContaining({
|
||||||
|
documentExhibitId:fatherDocument.id,evidenceAccepted:false,verification:expect.objectContaining({ status:'semantic_rejected' }),
|
||||||
|
})] })] })
|
||||||
|
|
||||||
const targetUpload = new FormData()
|
const targetUpload = new FormData()
|
||||||
targetUpload.append('file', new Blob([readFileSync(path.join(fixtureDir, 'google-patents-target-ocr.txt'))], { type:'text/plain' }), 'google-patents-source.txt')
|
targetUpload.append('file', new Blob([readFileSync(path.join(fixtureDir, 'google-patents-target-ocr.txt'))], { type:'text/plain' }), 'google-patents-source.txt')
|
||||||
@@ -317,9 +329,30 @@ suite('normalized level persistence API', () => {
|
|||||||
publishedAt:'1953-08-19',sourceCitation:'Google Patents · GB695913A',sourceUri:'https://patents.google.com/patent/GB695913A/en' }],
|
publishedAt:'1953-08-19',sourceCitation:'Google Patents · GB695913A',sourceUri:'https://patents.google.com/patent/GB695913A/en' }],
|
||||||
}) })
|
}) })
|
||||||
expect(accepted.status).toBe(201)
|
expect(accepted.status).toBe(201)
|
||||||
expect(await accepted.json()).toMatchObject({ status:'accepted',investigatorName:'Test Player',issues:[],claims:[expect.objectContaining({ evidence:[expect.objectContaining({
|
const acceptedBody=await accepted.json()
|
||||||
|
expect(acceptedBody).toMatchObject({ status:'accepted',investigatorName:'Test Player',issues:[] })
|
||||||
|
expect(acceptedBody.claims.flatMap((item:{ evidence:unknown[] }) => item.evidence)).toEqual(expect.arrayContaining([expect.objectContaining({
|
||||||
displayNumber:targetDocument.displayNumber,evidenceAccepted:true,sourceCitation:'Google Patents · GB695913A',publishedAt:'1953-08-19T00:00:00.000Z',
|
displayNumber:targetDocument.displayNumber,evidenceAccepted:true,sourceCitation:'Google Patents · GB695913A',publishedAt:'1953-08-19T00:00:00.000Z',
|
||||||
})] })] })
|
verification:expect.objectContaining({ status:'accepted' }),
|
||||||
|
})]))
|
||||||
|
|
||||||
|
// A later copy of the same correct source must be accepted on its own evaluation,
|
||||||
|
// even though the first copy already owns the one-time level-flag provenance.
|
||||||
|
const repeatedUpload=new FormData()
|
||||||
|
repeatedUpload.append('file',new Blob([readFileSync(path.join(fixtureDir,'google-patents-target-ocr.txt'))],{ type:'text/plain' }),'google-patents-second-copy.txt')
|
||||||
|
const repeatedDocument=await (await fetch(`${baseUrl}/api/levels/${reportState.id}/documents`,{ method:'POST',body:repeatedUpload })).json() as DocumentExhibit & { analysis:{ matchedFlags:string[];awardedFlags:string[] } }
|
||||||
|
expect(repeatedDocument.analysis).toMatchObject({ matchedFlags:['scene7.nils_inventor_proved'],awardedFlags:[] })
|
||||||
|
const repeatedState=await (await fetch(`${baseUrl}/api/levels/${reportState.id}`)).json() as CaseState
|
||||||
|
const repeatedConnectionId=randomUUID()
|
||||||
|
repeatedState.connections.push({ id:repeatedConnectionId,fromExhibitId:claim.id,toExhibitId:repeatedDocument.id,label:'Proof that Barricelli is named as the inventor on patent GB695913A.',tightness:65,tagStyle:'luggage',tagPosition:50,tagOffset:0 })
|
||||||
|
expect((await fetch(`${baseUrl}/api/levels/${repeatedState.id}`,{ method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify(repeatedState) })).status).toBe(200)
|
||||||
|
const repeatedReport=await (await fetch(`${baseUrl}/api/levels/${repeatedState.id}/report/submissions`,{ method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({
|
||||||
|
investigatorName:'Test Player',evidence:[{ connectionId:repeatedConnectionId,documentExhibitId:repeatedDocument.id,relationText:'Proof that Barricelli is named as the inventor on patent GB695913A.',
|
||||||
|
publishedAt:'1953-08-19',sourceCitation:'Google Patents · GB695913A',sourceUri:'https://patents.google.com/patent/GB695913A/en' }],
|
||||||
|
}) })).json()
|
||||||
|
expect(repeatedReport).toMatchObject({ status:'accepted',claims:[expect.objectContaining({ evidence:expect.arrayContaining([expect.objectContaining({
|
||||||
|
documentExhibitId:repeatedDocument.id,evidenceAccepted:true,verification:expect.objectContaining({ status:'accepted' }),
|
||||||
|
})]) })] })
|
||||||
judgeVerdict = { subject:'target',supports_claim:true,evidence_excerpt:'Ada Example patented a pocket telescope',confidence:.96 }
|
judgeVerdict = { subject:'target',supports_claim:true,evidence_excerpt:'Ada Example patented a pocket telescope',confidence:.96 }
|
||||||
judgeHttpStatus = 200
|
judgeHttpStatus = 200
|
||||||
})
|
})
|
||||||
|
|||||||
+77
-12
@@ -1,6 +1,6 @@
|
|||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
import type { Pool, PoolClient } from 'pg'
|
import type { Pool, PoolClient } from 'pg'
|
||||||
import type { CaseReport, CaseReportEvidence, CaseReportSubmissionInput, CaseReportSubmissionStatus, SourceFileType } from '../src/types.js'
|
import type { CaseReport, CaseReportEvidence, CaseReportSubmissionInput, CaseReportSubmissionStatus, EvidenceVerification, SourceFileType } from '../src/types.js'
|
||||||
|
|
||||||
type LevelRef = { id:string; board_id:string }
|
type LevelRef = { id:string; board_id:string }
|
||||||
|
|
||||||
@@ -21,6 +21,43 @@ function unfinishedRelation(value: string) {
|
|||||||
return !value.trim() || /^proof\s+that(?:\s*(?:…|\.{3}))?\s*$/iu.test(value.trim())
|
return !value.trim() || /^proof\s+that(?:\s*(?:…|\.{3}))?\s*$/iu.test(value.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RecognitionRow = {
|
||||||
|
evidence_accepted:boolean;extraction_status:'succeeded'|'unsupported'|'failed'|null
|
||||||
|
deterministic_evaluated:boolean;deterministic_matched:boolean|null;deterministic_score:string|null
|
||||||
|
matched_anchor_count:number|null;minimum_anchor_matches:number|null
|
||||||
|
semantic_status:'pending'|'succeeded'|'failed'|null;semantic_subject:'target'|'related'|'ambiguous'|'neither'|null
|
||||||
|
semantic_supports_claim:boolean|null;semantic_confidence:string|null;semantic_minimum_confidence:string|null
|
||||||
|
}
|
||||||
|
|
||||||
|
function verification(row:RecognitionRow):EvidenceVerification {
|
||||||
|
const score=row.deterministic_score === null ? undefined : Number(row.deterministic_score)
|
||||||
|
const metrics={ score,matchedMarkers:row.matched_anchor_count ?? undefined,requiredMarkers:row.minimum_anchor_matches ?? undefined }
|
||||||
|
if (row.evidence_accepted) return { status:'accepted',detail:row.deterministic_matched
|
||||||
|
? 'The extracted text matched the source fingerprint for this objective.'
|
||||||
|
: 'Semantic review found that this exhibit directly supports the target claim.',...metrics }
|
||||||
|
if (row.semantic_status === 'pending') return { status:'semantic_pending',detail:'Text was extracted, but semantic review is still pending.',...metrics }
|
||||||
|
if (row.semantic_status === 'failed') return { status:'semantic_failed',detail:'Text was extracted, but semantic review could not be completed. Retry document analysis.',...metrics }
|
||||||
|
if (row.semantic_status === 'succeeded') {
|
||||||
|
if (row.semantic_subject === 'target' && row.semantic_supports_claim) {
|
||||||
|
const confidence=Math.round(Number(row.semantic_confidence || 0) * 100)
|
||||||
|
const required=Math.round(Number(row.semantic_minimum_confidence || 0) * 100)
|
||||||
|
return { status:'semantic_rejected',detail:`Semantic review supported the claim, but confidence was ${confidence}% and this objective requires ${required}%.`,...metrics }
|
||||||
|
}
|
||||||
|
const subject = row.semantic_subject === 'related' ? 'a related person rather than the claim subject'
|
||||||
|
: row.semantic_subject === 'ambiguous' ? 'an ambiguous subject' : 'content that does not establish the target claim'
|
||||||
|
return { status:'semantic_rejected',detail:`Semantic review found ${subject}.`,...metrics }
|
||||||
|
}
|
||||||
|
if (row.deterministic_evaluated) {
|
||||||
|
const matched=row.matched_anchor_count || 0,required=row.minimum_anchor_matches || 0
|
||||||
|
const percentage=score === undefined ? null : Math.round(score * 100)
|
||||||
|
return { status:'text_not_matched',detail:`OCR succeeded, but this exhibit matched ${matched} of ${required} required source markers${percentage === null ? '' : ` (best similarity ${percentage}%)`}.`,...metrics }
|
||||||
|
}
|
||||||
|
if (row.extraction_status === 'failed') return { status:'ocr_unavailable',detail:'The image was saved, but OCR could not read usable text from it.' }
|
||||||
|
if (row.extraction_status === 'unsupported') return { status:'ocr_unavailable',detail:'This file format could not be checked automatically.' }
|
||||||
|
if (row.extraction_status === 'succeeded') return { status:'not_evaluated',detail:'Text was extracted, but no evidence-recognition rule evaluated this exhibit.' }
|
||||||
|
return { status:'not_evaluated',detail:'This exhibit has not been analyzed for the objective.' }
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadCaseReport(client: Pool | PoolClient, level: LevelRef): Promise<CaseReport | undefined> {
|
export async function loadCaseReport(client: Pool | PoolClient, level: LevelRef): Promise<CaseReport | undefined> {
|
||||||
const config = (await client.query<{ title:string; investigator_name:string; required_for_completion:boolean }>(
|
const config = (await client.query<{ title:string; investigator_name:string; required_for_completion:boolean }>(
|
||||||
'SELECT title,investigator_name,required_for_completion FROM osint.case_reports WHERE board_id=$1', [level.board_id])).rows[0]
|
'SELECT title,investigator_name,required_for_completion FROM osint.case_reports WHERE board_id=$1', [level.board_id])).rows[0]
|
||||||
@@ -30,17 +67,22 @@ export async function loadCaseReport(client: Pool | PoolClient, level: LevelRef)
|
|||||||
WHERE exhibit.board_id=$1 ORDER BY exhibit.created_at,exhibit.id`, [level.board_id])
|
WHERE exhibit.board_id=$1 ORDER BY exhibit.created_at,exhibit.id`, [level.board_id])
|
||||||
const evidence = await client.query<{
|
const evidence = await client.query<{
|
||||||
claim_exhibit_id:string; connection_id:string; document_exhibit_id:string; display_number:number; document_title:string
|
claim_exhibit_id:string; connection_id:string; document_exhibit_id:string; display_number:number; document_title:string
|
||||||
document_type_id:SourceFileType; relation_text:string; published_at:Date|null; citation_text:string; source_uri:string|null; evidence_accepted:boolean
|
document_type_id:SourceFileType; relation_text:string; published_at:Date|null; citation_text:string; source_uri:string|null
|
||||||
|
evidence_accepted:boolean;extraction_status:'succeeded'|'unsupported'|'failed'|null
|
||||||
|
deterministic_evaluated:boolean;deterministic_matched:boolean|null;deterministic_score:string|null
|
||||||
|
matched_anchor_count:number|null;minimum_anchor_matches:number|null
|
||||||
|
semantic_status:'pending'|'succeeded'|'failed'|null;semantic_subject:'target'|'related'|'ambiguous'|'neither'|null
|
||||||
|
semantic_supports_claim:boolean|null;semantic_confidence:string|null;semantic_minimum_confidence:string|null
|
||||||
}>(`SELECT claim.exhibit_id AS claim_exhibit_id,connection.id AS connection_id,document.exhibit_id AS document_exhibit_id,
|
}>(`SELECT claim.exhibit_id AS claim_exhibit_id,connection.id AS connection_id,document.exhibit_id AS document_exhibit_id,
|
||||||
citation.display_number,document.title AS document_title,document.document_type_id,COALESCE(connection.label,'') AS relation_text,
|
citation.display_number,document.title AS document_title,document.document_type_id,COALESCE(connection.label,'') AS relation_text,
|
||||||
document.published_at,document.citation_text,document.source_uri,
|
document.published_at,document.citation_text,document.source_uri,
|
||||||
(EXISTS (SELECT 1 FROM osint.level_flags flag JOIN osint.evidence_match_evaluations evaluation
|
(COALESCE(deterministic.matched,FALSE) OR COALESCE(semantic.status='succeeded' AND semantic.subject='target'
|
||||||
ON evaluation.id=flag.awarded_by_evidence_match_id
|
AND semantic.supports_claim AND semantic.confidence >= semantic.minimum_confidence,FALSE)) AS evidence_accepted,
|
||||||
WHERE flag.level_id=$2 AND evaluation.document_exhibit_id=document.exhibit_id)
|
extraction.status AS extraction_status,(deterministic.rule_id IS NOT NULL) AS deterministic_evaluated,
|
||||||
OR EXISTS (SELECT 1 FROM osint.level_flags flag JOIN osint.evidence_semantic_evaluations evaluation
|
deterministic.matched AS deterministic_matched,deterministic.score::text AS deterministic_score,
|
||||||
ON evaluation.id=flag.awarded_by_semantic_evaluation_id
|
deterministic.matched_anchor_count,deterministic.minimum_anchor_matches,
|
||||||
WHERE flag.level_id=$2 AND evaluation.document_exhibit_id=document.exhibit_id
|
semantic.status AS semantic_status,semantic.subject AS semantic_subject,semantic.supports_claim AS semantic_supports_claim,
|
||||||
AND evaluation.status='succeeded' AND evaluation.subject='target' AND evaluation.supports_claim)) AS evidence_accepted
|
semantic.confidence::text AS semantic_confidence,semantic.minimum_confidence::text AS semantic_minimum_confidence
|
||||||
FROM osint.claim_exhibits claim
|
FROM osint.claim_exhibits claim
|
||||||
JOIN osint.exhibits claim_exhibit ON claim_exhibit.id=claim.exhibit_id AND claim_exhibit.board_id=$1
|
JOIN osint.exhibits claim_exhibit ON claim_exhibit.id=claim.exhibit_id AND claim_exhibit.board_id=$1
|
||||||
JOIN osint.exhibit_connections connection ON connection.board_id=$1
|
JOIN osint.exhibit_connections connection ON connection.board_id=$1
|
||||||
@@ -48,6 +90,19 @@ export async function loadCaseReport(client: Pool | PoolClient, level: LevelRef)
|
|||||||
JOIN osint.document_exhibits document ON document.exhibit_id=CASE
|
JOIN osint.document_exhibits document ON document.exhibit_id=CASE
|
||||||
WHEN connection.from_exhibit_id=claim.exhibit_id THEN connection.to_exhibit_id ELSE connection.from_exhibit_id END
|
WHEN connection.from_exhibit_id=claim.exhibit_id THEN connection.to_exhibit_id ELSE connection.from_exhibit_id END
|
||||||
JOIN osint.exhibit_citations citation ON citation.board_id=$1 AND citation.exhibit_id=document.exhibit_id
|
JOIN osint.exhibit_citations citation ON citation.board_id=$1 AND citation.exhibit_id=document.exhibit_id
|
||||||
|
LEFT JOIN LATERAL (SELECT evaluation.rule_id,evaluation.matched,evaluation.score,evaluation.matched_anchor_count,rule.minimum_anchor_matches
|
||||||
|
FROM osint.evidence_match_evaluations evaluation
|
||||||
|
JOIN osint.evidence_match_rules rule ON rule.id=evaluation.rule_id AND rule.board_id=$1 AND rule.enabled
|
||||||
|
WHERE evaluation.level_id=$2 AND evaluation.document_exhibit_id=document.exhibit_id
|
||||||
|
ORDER BY evaluation.matched DESC,evaluation.score DESC,evaluation.evaluated_at DESC LIMIT 1) deterministic ON TRUE
|
||||||
|
LEFT JOIN LATERAL (SELECT evaluation.status,evaluation.subject,evaluation.supports_claim,evaluation.confidence,rule.minimum_confidence
|
||||||
|
FROM osint.evidence_semantic_evaluations evaluation
|
||||||
|
JOIN osint.evidence_semantic_rules rule ON rule.id=evaluation.rule_id AND rule.board_id=$1 AND rule.enabled
|
||||||
|
WHERE evaluation.level_id=$2 AND evaluation.document_exhibit_id=document.exhibit_id
|
||||||
|
ORDER BY (evaluation.status='succeeded' AND evaluation.subject='target' AND evaluation.supports_claim
|
||||||
|
AND evaluation.confidence >= rule.minimum_confidence) DESC,evaluation.updated_at DESC LIMIT 1) semantic ON TRUE
|
||||||
|
LEFT JOIN LATERAL (SELECT candidate.status FROM osint.asset_text_extractions candidate
|
||||||
|
WHERE candidate.asset_id=document.asset_id ORDER BY candidate.updated_at DESC LIMIT 1) extraction ON TRUE
|
||||||
ORDER BY claim_exhibit.created_at,claim.exhibit_id,citation.display_number,connection.created_at`, [level.board_id,level.id])
|
ORDER BY claim_exhibit.created_at,claim.exhibit_id,citation.display_number,connection.created_at`, [level.board_id,level.id])
|
||||||
const latest = (await client.query<{ id:string;status:CaseReportSubmissionStatus; feedback:string }>(
|
const latest = (await client.query<{ id:string;status:CaseReportSubmissionStatus; feedback:string }>(
|
||||||
'SELECT id,status,feedback FROM osint.case_report_submissions WHERE level_id=$1 ORDER BY submitted_at DESC,id DESC LIMIT 1', [level.id])).rows[0]
|
'SELECT id,status,feedback FROM osint.case_report_submissions WHERE level_id=$1 ORDER BY submitted_at DESC,id DESC LIMIT 1', [level.id])).rows[0]
|
||||||
@@ -58,7 +113,7 @@ export async function loadCaseReport(client: Pool | PoolClient, level: LevelRef)
|
|||||||
connectionId:row.connection_id,documentExhibitId:row.document_exhibit_id,displayNumber:row.display_number,
|
connectionId:row.connection_id,documentExhibitId:row.document_exhibit_id,displayNumber:row.display_number,
|
||||||
documentTitle:row.document_title,fileType:row.document_type_id,relationText:row.relation_text,
|
documentTitle:row.document_title,fileType:row.document_type_id,relationText:row.relation_text,
|
||||||
publishedAt:row.published_at?.toISOString(),sourceCitation:row.citation_text || undefined,sourceUri:row.source_uri || undefined,
|
publishedAt:row.published_at?.toISOString(),sourceCitation:row.citation_text || undefined,sourceUri:row.source_uri || undefined,
|
||||||
evidenceAccepted:row.evidence_accepted,
|
evidenceAccepted:row.evidence_accepted,verification:verification(row),
|
||||||
}])
|
}])
|
||||||
return { title:config.title,investigatorName:config.investigator_name,requiredForCompletion:config.required_for_completion,
|
return { title:config.title,investigatorName:config.investigator_name,requiredForCompletion:config.required_for_completion,
|
||||||
status:latest?.status || 'draft',feedback:latest?.feedback,issues,
|
status:latest?.status || 'draft',feedback:latest?.feedback,issues,
|
||||||
@@ -111,7 +166,11 @@ export async function submitCaseReport(pool: Pool, levelSlug: string, rawInput:
|
|||||||
if (!assembled.claims.length) blockingIssues.add('missing_claim')
|
if (!assembled.claims.length) blockingIssues.add('missing_claim')
|
||||||
for (const claim of assembled.claims) {
|
for (const claim of assembled.claims) {
|
||||||
const accepted = claim.evidence.filter(item => item.evidenceAccepted)
|
const accepted = claim.evidence.filter(item => item.evidenceAccepted)
|
||||||
if (!accepted.length) { blockingIssues.add('missing_accepted_evidence'); continue }
|
if (!accepted.length) {
|
||||||
|
blockingIssues.add('missing_accepted_evidence')
|
||||||
|
blockingIssues.add(claim.evidence.length ? 'connected_evidence_unverified' : 'missing_connected_evidence')
|
||||||
|
continue
|
||||||
|
}
|
||||||
for (const item of accepted) {
|
for (const item of accepted) {
|
||||||
if (unfinishedRelation(item.relationText)) blockingIssues.add('unfinished_relation')
|
if (unfinishedRelation(item.relationText)) blockingIssues.add('unfinished_relation')
|
||||||
if (!item.publishedAt) blockingIssues.add('missing_date')
|
if (!item.publishedAt) blockingIssues.add('missing_date')
|
||||||
@@ -122,7 +181,13 @@ export async function submitCaseReport(pool: Pool, levelSlug: string, rawInput:
|
|||||||
let feedback:string
|
let feedback:string
|
||||||
if (pendingGoals || !connectedAccepted) {
|
if (pendingGoals || !connectedAccepted) {
|
||||||
status='evidence_insufficient'
|
status='evidence_insufficient'
|
||||||
feedback='The report does not yet connect the claim to evidence that proves it. Find the source, add it to the board, and connect it with red thread.'
|
const emptyClaim=assembled.claims.find(claim => !claim.evidence.length)
|
||||||
|
const rejected=assembled.claims.flatMap(claim => claim.evidence).find(item => !item.evidenceAccepted)
|
||||||
|
feedback=emptyClaim
|
||||||
|
? 'No source document is connected to the claim. Return to the board and attach one with red thread.'
|
||||||
|
: rejected
|
||||||
|
? `Exhibit ${rejected.displayNumber} is connected to the claim, but it was not accepted: ${rejected.verification.detail}`
|
||||||
|
: 'The connected evidence was recognized, but another required level objective is still incomplete.'
|
||||||
} else if (blockingIssues.size) {
|
} else if (blockingIssues.size) {
|
||||||
status='evidence_accepted_report_incomplete'
|
status='evidence_accepted_report_incomplete'
|
||||||
feedback=blockingIssues.has('missing_date') || blockingIssues.has('missing_source')
|
feedback=blockingIssues.has('missing_date') || blockingIssues.has('missing_source')
|
||||||
|
|||||||
+3
-2
@@ -1128,8 +1128,9 @@ function CaseReportPanel({ report, defaultInvestigator, hasNext, onClose, onSubm
|
|||||||
<h3><span>CLAIM {claimIndex + 1}</span>{claim.statement}</h3>
|
<h3><span>CLAIM {claimIndex + 1}</span>{claim.statement}</h3>
|
||||||
<h4>EVIDENCE</h4>
|
<h4>EVIDENCE</h4>
|
||||||
{claim.evidence.length === 0 ? <div className="report-empty-evidence">No source evidence is connected to this claim. Return to the board and use red thread to attach a document.</div>
|
{claim.evidence.length === 0 ? <div className="report-empty-evidence">No source evidence is connected to this claim. Return to the board and use red thread to attach a document.</div>
|
||||||
: claim.evidence.map(item => { const draft=draftByConnection.get(item.connectionId)!; return <article className={`report-evidence ${item.evidenceAccepted ? 'verified' : ''}`} key={item.connectionId}>
|
: claim.evidence.map(item => { const draft=draftByConnection.get(item.connectionId)!; const rejected=report.status !== 'draft' && !item.evidenceAccepted; return <article className={`report-evidence ${item.evidenceAccepted ? 'verified' : rejected ? 'rejected' : ''}`} key={item.connectionId}>
|
||||||
<div className="report-evidence-heading"><b>Exhibit {item.displayNumber}</b><span>{item.fileType.replaceAll('_',' ')} · {item.documentTitle}</span>{item.evidenceAccepted && <em>CONTENT VERIFIED</em>}</div>
|
<div className="report-evidence-heading"><b>Exhibit {item.displayNumber}</b><span>{item.fileType.replaceAll('_',' ')} · {item.documentTitle}</span>{item.evidenceAccepted ? <em>CONTENT VERIFIED</em> : rejected ? <em className="rejected">NOT VERIFIED</em> : null}</div>
|
||||||
|
{rejected && <p className="report-evidence-diagnostic">{item.verification.detail}</p>}
|
||||||
<label><span>EVIDENTIARY STATEMENT</span><textarea aria-label={`Evidence statement for Exhibit ${item.displayNumber}`} rows={2} value={draft.relationText} onChange={event => updateEvidence(item.connectionId,{ relationText:event.target.value })}/></label>
|
<label><span>EVIDENTIARY STATEMENT</span><textarea aria-label={`Evidence statement for Exhibit ${item.displayNumber}`} rows={2} value={draft.relationText} onChange={event => updateEvidence(item.connectionId,{ relationText:event.target.value })}/></label>
|
||||||
<div className="report-fields"><label><span>DATED</span><input aria-label={`Date for Exhibit ${item.displayNumber}`} type="date" value={draft.publishedAt || ''} onChange={event => updateEvidence(item.connectionId,{ publishedAt:event.target.value || undefined })}/></label>
|
<div className="report-fields"><label><span>DATED</span><input aria-label={`Date for Exhibit ${item.displayNumber}`} type="date" value={draft.publishedAt || ''} onChange={event => updateEvidence(item.connectionId,{ publishedAt:event.target.value || undefined })}/></label>
|
||||||
<label><span>SOURCE / PUBLICATION</span><input aria-label={`Source for Exhibit ${item.displayNumber}`} value={draft.sourceCitation || ''} placeholder="e.g. Google Patents · GB695913A" onChange={event => updateEvidence(item.connectionId,{ sourceCitation:event.target.value })}/></label></div>
|
<label><span>SOURCE / PUBLICATION</span><input aria-label={`Source for Exhibit ${item.displayNumber}`} value={draft.sourceCitation || ''} placeholder="e.g. Google Patents · GB695913A" onChange={event => updateEvidence(item.connectionId,{ sourceCitation:event.target.value })}/></label></div>
|
||||||
|
|||||||
+2
-1
@@ -377,8 +377,9 @@ button:focus-visible { outline: 2px solid #e99a44; outline-offset: 2px; }
|
|||||||
.case-report-letterhead { position:relative; padding-bottom:22px; border-bottom:3px double #343b37; text-align:center; }.case-report-letterhead small,.case-report-letterhead span { display:block; color:#59625d; font:7px IBM Plex Mono; letter-spacing:.16em; }.case-report-letterhead h2 { margin:9px 0 7px; color:#1d2622; font:30px Special Elite; letter-spacing:.11em; }
|
.case-report-letterhead { position:relative; padding-bottom:22px; border-bottom:3px double #343b37; text-align:center; }.case-report-letterhead small,.case-report-letterhead span { display:block; color:#59625d; font:7px IBM Plex Mono; letter-spacing:.16em; }.case-report-letterhead h2 { margin:9px 0 7px; color:#1d2622; font:30px Special Elite; letter-spacing:.11em; }
|
||||||
.report-claim { margin-top:30px; }.report-claim h3 { margin:0 0 22px; font:21px/1.45 Special Elite; }.report-claim h3 span { display:block; margin-bottom:5px; color:#915c30; font:7px IBM Plex Mono; letter-spacing:.14em; }.report-claim h4 { margin:0; padding-bottom:6px; border-bottom:1px solid #4c554f; font:600 8px IBM Plex Mono; letter-spacing:.15em; }
|
.report-claim { margin-top:30px; }.report-claim h3 { margin:0 0 22px; font:21px/1.45 Special Elite; }.report-claim h3 span { display:block; margin-bottom:5px; color:#915c30; font:7px IBM Plex Mono; letter-spacing:.14em; }.report-claim h4 { margin:0; padding-bottom:6px; border-bottom:1px solid #4c554f; font:600 8px IBM Plex Mono; letter-spacing:.15em; }
|
||||||
.report-empty-evidence { margin-top:13px; padding:17px; border:1px dashed #8a6550; color:#76513c; background:#d9d1bd; font:10px/1.5 IBM Plex Mono; }
|
.report-empty-evidence { margin-top:13px; padding:17px; border:1px dashed #8a6550; color:#76513c; background:#d9d1bd; font:10px/1.5 IBM Plex Mono; }
|
||||||
.report-evidence { margin-top:14px; padding:14px 16px 16px; border-left:4px solid #8b5b38; background:#e1ddcfcc; box-shadow:0 1px #fff8; }.report-evidence.verified { border-left-color:#46705c; }
|
.report-evidence { margin-top:14px; padding:14px 16px 16px; border-left:4px solid #8b5b38; background:#e1ddcfcc; box-shadow:0 1px #fff8; }.report-evidence.verified { border-left-color:#46705c; }.report-evidence.rejected { border-left-color:#913d35;background:#e5d2c7cc; }
|
||||||
.report-evidence-heading { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:10px; margin-bottom:12px; }.report-evidence-heading b { color:#25332d; font:14px Special Elite; }.report-evidence-heading span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#646c67; font:8px IBM Plex Mono; text-transform:uppercase; }.report-evidence-heading em { padding:3px 5px; border:1px solid #577665; color:#436753; font:600 6px IBM Plex Mono; letter-spacing:.08em; font-style:normal; }
|
.report-evidence-heading { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:10px; margin-bottom:12px; }.report-evidence-heading b { color:#25332d; font:14px Special Elite; }.report-evidence-heading span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#646c67; font:8px IBM Plex Mono; text-transform:uppercase; }.report-evidence-heading em { padding:3px 5px; border:1px solid #577665; color:#436753; font:600 6px IBM Plex Mono; letter-spacing:.08em; font-style:normal; }
|
||||||
|
.report-evidence-heading em.rejected { border-color:#98534b;color:#813a34; }.report-evidence-diagnostic { margin:-3px 0 13px;padding:9px 10px;border:1px solid #b78376;background:#ead8cc;color:#6f302d;font:9px/1.5 IBM Plex Mono; }
|
||||||
.report-evidence label,.report-investigator { display:grid; gap:4px; margin-top:9px; }.report-evidence label > span,.report-investigator > span { color:#5c655f; font:600 7px IBM Plex Mono; letter-spacing:.11em; }.report-evidence input,.report-evidence textarea,.report-investigator input { width:100%; min-width:0; padding:7px 8px; border:0; border-bottom:1px solid #747b75; outline:0; background:#f2eee0a8; color:#1d2622; font:12px/1.45 Special Elite; resize:vertical; }.report-evidence input:focus,.report-evidence textarea:focus,.report-investigator input:focus { border-bottom-color:#9b582d; background:#f6f0dc; box-shadow:inset 3px 0 #b2784a; }
|
.report-evidence label,.report-investigator { display:grid; gap:4px; margin-top:9px; }.report-evidence label > span,.report-investigator > span { color:#5c655f; font:600 7px IBM Plex Mono; letter-spacing:.11em; }.report-evidence input,.report-evidence textarea,.report-investigator input { width:100%; min-width:0; padding:7px 8px; border:0; border-bottom:1px solid #747b75; outline:0; background:#f2eee0a8; color:#1d2622; font:12px/1.45 Special Elite; resize:vertical; }.report-evidence input:focus,.report-evidence textarea:focus,.report-investigator input:focus { border-bottom-color:#9b582d; background:#f6f0dc; box-shadow:inset 3px 0 #b2784a; }
|
||||||
.report-fields { display:grid; grid-template-columns:155px minmax(0,1fr); gap:13px; }.report-investigator { margin-top:30px; padding-top:13px; border-top:1px solid #4c554f; }.report-investigator input { max-width:390px; }
|
.report-fields { display:grid; grid-template-columns:155px minmax(0,1fr); gap:13px; }.report-investigator { margin-top:30px; padding-top:13px; border-top:1px solid #4c554f; }.report-investigator input { max-width:390px; }
|
||||||
.report-verdict { margin-top:28px; padding:17px 19px; border:2px solid #874339; background:#e0c7b9; transform:rotate(-.25deg); }.report-verdict small { color:#7c352f; font:700 8px IBM Plex Mono; letter-spacing:.13em; }.report-verdict p { margin:9px 0 0; font:15px/1.5 Special Elite; }.report-verdict.accepted { border-color:#47705c; background:#d2ddcf; }.report-verdict.accepted small { color:#315b48; }
|
.report-verdict { margin-top:28px; padding:17px 19px; border:2px solid #874339; background:#e0c7b9; transform:rotate(-.25deg); }.report-verdict small { color:#7c352f; font:700 8px IBM Plex Mono; letter-spacing:.13em; }.report-verdict p { margin:9px 0 0; font:15px/1.5 Special Elite; }.report-verdict.accepted { border-color:#47705c; background:#d2ddcf; }.report-verdict.accepted small { color:#315b48; }
|
||||||
|
|||||||
@@ -179,6 +179,15 @@ export interface LevelGoal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CaseReportSubmissionStatus = 'evidence_insufficient' | 'evidence_accepted_report_incomplete' | 'accepted'
|
export type CaseReportSubmissionStatus = 'evidence_insufficient' | 'evidence_accepted_report_incomplete' | 'accepted'
|
||||||
|
export type EvidenceVerificationStatus = 'accepted' | 'not_evaluated' | 'ocr_unavailable' | 'text_not_matched' | 'semantic_pending' | 'semantic_failed' | 'semantic_rejected'
|
||||||
|
|
||||||
|
export interface EvidenceVerification {
|
||||||
|
status: EvidenceVerificationStatus
|
||||||
|
detail: string
|
||||||
|
score?: number
|
||||||
|
matchedMarkers?: number
|
||||||
|
requiredMarkers?: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface CaseReportEvidence {
|
export interface CaseReportEvidence {
|
||||||
connectionId: string
|
connectionId: string
|
||||||
@@ -191,6 +200,7 @@ export interface CaseReportEvidence {
|
|||||||
sourceCitation?: string
|
sourceCitation?: string
|
||||||
sourceUri?: string
|
sourceUri?: string
|
||||||
evidenceAccepted: boolean
|
evidenceAccepted: boolean
|
||||||
|
verification: EvidenceVerification
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CaseReportClaim {
|
export interface CaseReportClaim {
|
||||||
|
|||||||
Reference in New Issue
Block a user