Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ Sections per release: **Added** (new features), **Changed**

### Added

- **Case dashboard — cross-article claim links + clearer vocabulary
(Phase 26 prep, T1.3).** The claims list now surfaces the
**cross-article relationship links** that already existed but were
never shown — each claim's `supports` / `updates` / `duplicates`
edges (both directions, naming the linked claim). Contradiction
keeps its own ⚠ badge. And the two sections a reviewer found opaque
now carry one-line explainers: the **Evidence** list ("each captured
source: what it claims and which outbound URLs it cites; grouped by
independent origin") and the **Case graph** ("the same sources
plotted, plus the entities in them and how they interconnect"); the
outbound-links line reads "cites N outbound URLs (M also in this
case) · cited by K case sources" instead of the ambiguous "links
to…". Display-only — the links already lived in the store.

- **Corpus brief — download + provenance surfacing (Phase 26 prep,
T1.1/T1.2).** The stored case brief gains **Download .md / .json**
controls beside "Publish brief…" (the `.md` is the same readable,
Expand Down
2 changes: 2 additions & 0 deletions docs/SMOKE_TEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -1307,6 +1307,8 @@ stored brief (run "Analyze corpus…" once).
| P26.b | **Download .json** | ✅ a `case-brief-<slug>.json` downloads; contains `brief`, `grounding`, `model`, `members`, `inputHash` |
| P26.c | Inspect the rendered brief on the dashboard | ✅ each **crux** shows its grounded evidence quotes with a source link; each **load-bearing** claim shows a "Source:" link (and a "Claim:" text when `claim_ref` resolves); each **position** shows "Held by:" source links |
| P26.d | Click a source link | ✅ opens the captured source's original URL in a new tab; nothing publishes |
| P26.e | A case with a `supports`/`updates`/`duplicates` link between two claims → case dashboard ▸ Claims | ✅ the claim rows show a "→ supports: <other claim>" / "← updated by: <other>" badge naming the linked claim; contradiction still shows its ⚠ badge separately |
| P26.f | Read the Evidence and Case-graph section headers | ✅ each has a one-line explainer; the source's link line reads "cites N outbound URLs (M also in this case) · cited by K case sources" |

---

Expand Down
4 changes: 4 additions & 0 deletions src/portal/case-graph-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export function renderCaseGraph(host, { data, callbacks = {} }) {
const positions = layoutCaseGraph(graph, { size: SIZE });

block.appendChild(el('h3', 'xr-case__heading', 'Case graph — articles, entities, and how they connect'));
block.appendChild(el('div', 'xr-case__explainer',
'The same sources as the Evidence list above, plotted as a graph — plus the people and '
+ 'organizations tagged or claimed in them, which sources share entities (co-tag links), and '
+ 'where claims contradict. Click an entity for its dossier, an article to open it in the reader.'));
const c = graph.counts;
block.appendChild(el('div', 'xr-view__dossier-line',
`${c.articles} article${c.articles === 1 ? '' : 's'} · ${c.entities} entit${c.entities === 1 ? 'y' : 'ies'} · `
Expand Down
53 changes: 53 additions & 0 deletions src/portal/case-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,44 @@ function contradictedCoords(items) {
return out;
}

// Claim titles by coordinate — so a cross-article link can name the
// claim it points at (built from the case's own claim items).
function claimTitleByCoord(items) {
const map = new Map();
for (const item of items) {
if (item.typeKey === 'claim' && item.claimCoord) map.set(item.claimCoord, item.title || '');
}
return map;
}

// Non-contradiction relationship links (supports / updates / duplicates)
// touching each claim coordinate, both directions — the cross-article
// links the dashboard never surfaced. Contradiction keeps its own ⚠
// badge; the diachronic revision/* edges are a forensic surface,
// excluded here. Returns coord → [{relationship, dir, otherCoord}].
const RELATED_RELATIONSHIPS = new Set(['supports', 'updates', 'duplicates']);
function relatedLinksByCoord(items) {
const map = new Map();
const add = (coord, entry) => {
if (!coord) return;
if (!map.has(coord)) map.set(coord, []);
map.get(coord).push(entry);
};
for (const item of items) {
if (item.typeKey !== 'link' || !RELATED_RELATIONSHIPS.has(item.relationship)) continue;
add(item.sourceCoord, { relationship: item.relationship, dir: 'out', otherCoord: item.targetCoord });
add(item.targetCoord, { relationship: item.relationship, dir: 'in', otherCoord: item.sourceCoord });
}
return map;
}

// "supports →" / "← supported by" phrasing per relationship + direction.
const RELATED_PHRASE = {
supports: { out: 'supports', in: 'supported by' },
updates: { out: 'updates', in: 'updated by' },
duplicates: { out: 'duplicates', in: 'duplicated by' }
};

/**
* @param {HTMLElement} host
* @param {object} params
Expand Down Expand Up @@ -301,6 +339,8 @@ export function renderCaseView(host, params) {
// --- claims with stance/⚠ badges ---
const assessments = latestAssessmentByCoord(items);
const contradicted = contradictedCoords(items);
const relatedLinks = relatedLinksByCoord(items);
const claimTitles = claimTitleByCoord(items);
const claims = caseItems.filter((i) => i.typeKey === 'claim');
const section = el('div', 'xr-case__claims');
section.appendChild(el('h3', 'xr-case__heading', `Claims (${claims.length})`));
Expand Down Expand Up @@ -333,6 +373,19 @@ export function renderCaseView(host, params) {
}
row.appendChild(headRow);
if (item.sub) row.appendChild(el('div', 'xr-row__sub', truncate(item.sub, 240)));
// Cross-article relationship links (supports/updates/duplicates)
// touching this claim — the links the dashboard never surfaced.
const related = item.claimCoord ? relatedLinks.get(item.claimCoord) : null;
if (related && related.length) {
const relRow = el('div', 'xr-case__related');
for (const r of related.slice(0, 6)) {
const phrase = (RELATED_PHRASE[r.relationship] || {})[r.dir] || r.relationship;
const other = (r.otherCoord && claimTitles.get(r.otherCoord)) || 'another claim';
relRow.appendChild(el('span', 'xr-badge xr-badge--muted',
`${r.dir === 'out' ? '→' : '←'} ${phrase}: ${truncate(other, 60)}`));
}
row.appendChild(relRow);
}
list.appendChild(row);
}
if (claims.length === 0) {
Expand Down
10 changes: 7 additions & 3 deletions src/portal/evidence-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export function renderEvidenceBlock(host, dossierOrId, opts = {}) {
if (ev.articles.length === 0 && ev.unprocessed_sources.length === 0) { block.remove(); return; }

block.appendChild(el('h3', 'xr-case__heading', 'Evidence — sources collapsed by origin'));
block.appendChild(el('div', 'xr-case__explainer',
'Each captured source in this case: what it claims, and which outbound URLs it cites. '
+ 'Sources reporting the same fact are grouped by their independent origin, so N reports '
+ 'tracing to one origin count as one.'));
block.appendChild(el('div', 'xr-view__dossier-line',
`${ev.coverage.articles} sources · ${ev.coverage.attested_articles} attested · `
+ `${ev.coverage.articles_with_audit} audited · ${ev.coverage.unprocessed} unprocessed`));
Expand Down Expand Up @@ -105,11 +109,11 @@ export function renderEvidenceBlock(host, dossierOrId, opts = {}) {
if (lnk && (lnk.captured || lnk.linked_by.length > 0)) {
const bits = [];
if (lnk.captured) {
bits.push(`links to ${lnk.external} external source${lnk.external === 1 ? '' : 's'}`
+ (lnk.corpus_links.length ? ` (${lnk.corpus_links.length} in this case)` : ''));
bits.push(`cites ${lnk.external} outbound URL${lnk.external === 1 ? '' : 's'}`
+ (lnk.corpus_links.length ? ` (${lnk.corpus_links.length} also in this case)` : ''));
}
if (lnk.linked_by.length > 0) {
bits.push(`linked from ${lnk.linked_by.length} case article${lnk.linked_by.length === 1 ? '' : 's'}`);
bits.push(`cited by ${lnk.linked_by.length} case source${lnk.linked_by.length === 1 ? '' : 's'}`);
}
li.appendChild(el('div', 'xr-inspector__mono', bits.join(' · ')));
}
Expand Down
2 changes: 2 additions & 0 deletions src/portal/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,8 @@ body.xr-portal {
.xr-synth__prov-label { color: var(--xr-text-dim); margin-right: 4px; }
.xr-synth__src { color: var(--xr-accent, #22d3ee); text-decoration: none; }
.xr-synth__src:hover { text-decoration: underline; }
.xr-case__explainer { color: var(--xr-text-dim); font-size: 12px; margin: 2px 0 8px; max-width: 70ch; line-height: 1.4; }
.xr-case__related { display: flex; gap: 6px; flex-wrap: wrap; margin: 4px 0 2px; }
.xr-synth__prop { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 6px 0; border-bottom: 1px solid var(--xr-border); }
.xr-synth__prop--rejected { color: var(--xr-text-dim); }
.xr-synth__prop-desc { flex: 1 1 auto; min-width: 200px; }
Expand Down
Loading