diff --git a/README.md b/README.md index 8b5fe7e..0028718 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ -# artemis2 \ No newline at end of file +# Artemis II Knowledge Provenance Atlas (C6) + +A browser-based, visual, shareable prototype focused on **evidence provenance** for Artemis II claims using public, citable sources. + +## Quick start + +Open `./index.html` in a browser. + +No build step is required. + +## What is implemented + +- Browser app centered on **C6 Knowledge Provenance Atlas** +- Interactive visual graph of claims, mission elements, and sources +- Confidence and evidence badges with a “Why this confidence?” explainer +- Topic, confidence, source-type, and recency filters +- Supporting/contradicting source comparison cards +- Shareable deep links to a selected claim (`#claim:`) +- Trust controls: + - Provenance completeness checks + - Broken-link format checks + - Duplicate/near-duplicate source detection + - Ambiguous-claim manual review queue +- Seeded public-source inventory and implementation metadata + +## Scope guardrails + +- Public, non-classified, citable sources only +- Every claim includes source links, retrieval date, and confidence +- Educational and analytical prototype only (not operational NASA mission truth) diff --git a/index.html b/index.html new file mode 100644 index 0000000..3e56beb --- /dev/null +++ b/index.html @@ -0,0 +1,81 @@ + + + + + + Artemis II Knowledge Provenance Atlas + + + +
+

Artemis II Knowledge Provenance Atlas

+

+ Browser-based, visual, shareable exploration of public Artemis II claims and evidence. +

+

+ Disclaimer: This prototype is for educational interpretation of public sources, not operational + NASA mission truth. +

+
+ +
+
+

Filters

+ + + + +
+ +
+

Visual Graph

+ +
+ +
+

Claims

+
+
+ +
+

Claim Detail

+
+ Select a claim to inspect supporting and contradicting evidence. +
+
+ +
+

Quality & Trust Controls

+
    +
    + +
    +

    Launch Checklist

    +
      +
      +
      + + + + diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..b41106e --- /dev/null +++ b/src/app.js @@ -0,0 +1,394 @@ +import { + audiences, + claims, + elements, + launchChecklist, + sourceInventory, + sourcePolicy, + successCriteria, +} from "./data.js"; + +const state = { + topic: "all", + minConfidence: 0, + sourceType: "all", + recency: "all", + selectedClaimId: null, +}; + +const el = { + topic: document.getElementById("filter-topic"), + confidence: document.getElementById("filter-confidence"), + sourceType: document.getElementById("filter-source-type"), + recency: document.getElementById("filter-recency"), + claimsList: document.getElementById("claims-list"), + claimDetail: document.getElementById("claim-detail"), + trustList: document.getElementById("trust-list"), + graph: document.getElementById("graph"), + launchChecklist: document.getElementById("launch-checklist"), +}; + +const now = new Date("2026-04-02"); + +function confidenceLabel(v) { + return v === 3 ? "High" : v === 2 ? "Medium" : "Low"; +} + +function confidenceClass(v) { + return v === 3 ? "high" : v === 2 ? "medium" : "low"; +} + +function toDate(str) { + return new Date(`${str}T00:00:00Z`); +} + +function yearsAgo(date) { + return (now - date) / (1000 * 60 * 60 * 24 * 365.25); +} + +function sourceById(id) { + return sourceInventory.find((s) => s.id === id); +} + +function claimPassesFilters(claim) { + if (state.topic !== "all" && claim.topic !== state.topic) return false; + if (claim.confidence < state.minConfidence) return false; + + const linkedSources = claim.sources.map((s) => sourceById(s.sourceId)).filter(Boolean); + + if (state.sourceType !== "all" && !linkedSources.some((s) => s.type === state.sourceType)) return false; + + if (state.recency !== "all") { + const maxYears = state.recency === "1y" ? 1 : 2; + const hasRecent = linkedSources.some((s) => yearsAgo(toDate(s.retrievedAt)) <= maxYears); + if (!hasRecent) return false; + } + + return true; +} + +function filteredClaims() { + return claims.filter(claimPassesFilters); +} + +function setHashForClaim(claimId) { + history.replaceState(null, "", `#claim:${claimId}`); +} + +function claimFromHash() { + const hash = window.location.hash || ""; + if (!hash.startsWith("#claim:")) return null; + return hash.replace("#claim:", ""); +} + +function populateFilterOptions() { + const topics = Array.from(new Set(["all", ...claims.map((c) => c.topic)])); + el.topic.innerHTML = topics + .map((topic) => ``) + .join(""); + + const sourceTypes = Array.from(new Set(["all", ...sourceInventory.map((s) => s.type)])); + el.sourceType.innerHTML = sourceTypes + .map((type) => ``) + .join(""); +} + +function renderClaims() { + const list = filteredClaims(); + if (!list.length) { + el.claimsList.innerHTML = "

      No claims match current filters.

      "; + return; + } + + if (!state.selectedClaimId || !list.some((c) => c.id === state.selectedClaimId)) { + state.selectedClaimId = list[0].id; + } + + el.claimsList.innerHTML = list + .map( + (claim) => ` + + `, + ) + .join(""); + + for (const button of el.claimsList.querySelectorAll(".claim-button")) { + button.addEventListener("click", () => { + state.selectedClaimId = button.dataset.claimId; + setHashForClaim(state.selectedClaimId); + renderAll(); + }); + } +} + +function renderClaimDetail() { + const claim = claims.find((c) => c.id === state.selectedClaimId); + if (!claim) { + el.claimDetail.textContent = "Select a claim to inspect details."; + return; + } + + const supports = claim.sources + .filter((s) => s.relation === "supports") + .map((s) => sourceById(s.sourceId)) + .filter(Boolean); + const contradicts = claim.sources + .filter((s) => s.relation === "contradicts") + .map((s) => sourceById(s.sourceId)) + .filter(Boolean); + + const sourceCard = (source, contradict = false) => ` +
      + ${source.title}
      + ${source.reliability} + ${source.type} · retrieved ${source.retrievedAt}
      + Open source +
      + `; + + el.claimDetail.innerHTML = ` +

      Claim: ${claim.statement}

      +

      ${confidenceLabel(claim.confidence)}

      +

      Why this confidence? ${claim.whyConfidence}

      +

      Supporting evidence

      + ${supports.map((s) => sourceCard(s)).join("") || "

      None

      "} +

      Contradicting evidence

      + ${contradicts.map((s) => sourceCard(s, true)).join("") || "

      None

      "} +

      Shareable deep link: ${window.location.origin}${window.location.pathname}#claim:${claim.id}

      + `; +} + +function drawNode(svg, { x, y, r, label, nodeClass, id, onClick }) { + const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); + g.setAttribute("data-id", id); + g.style.cursor = "pointer"; + + const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + circle.setAttribute("cx", x); + circle.setAttribute("cy", y); + circle.setAttribute("r", r); + circle.setAttribute( + "fill", + nodeClass === "claim" + ? "#214163" + : nodeClass === "source" + ? "#2b4d37" + : "#4d3f2b", + ); + circle.setAttribute("stroke", state.selectedClaimId === id ? "#5bc0ff" : "#6f88a8"); + circle.setAttribute("stroke-width", state.selectedClaimId === id ? "3" : "1"); + + const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); + text.setAttribute("x", x); + text.setAttribute("y", y + 4); + text.setAttribute("font-size", "10"); + text.setAttribute("text-anchor", "middle"); + text.setAttribute("fill", "#e6edf3"); + text.textContent = label; + + g.append(circle, text); + g.addEventListener("click", onClick); + svg.appendChild(g); +} + +function drawLine(svg, x1, y1, x2, y2, relation) { + const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); + line.setAttribute("x1", x1); + line.setAttribute("y1", y1); + line.setAttribute("x2", x2); + line.setAttribute("y2", y2); + line.setAttribute("stroke", relation === "contradicts" ? "#ff7b72" : "#7ee787"); + line.setAttribute("stroke-width", "2"); + if (relation === "contradicts") { + line.setAttribute("stroke-dasharray", "5 3"); + } + svg.appendChild(line); +} + +function renderGraph() { + const list = filteredClaims(); + el.graph.innerHTML = ""; + + if (!list.length) return; + + const claimNodes = list.map((claim, idx) => ({ + id: claim.id, + x: 210 + idx * 250, + y: 130, + r: 28, + label: claim.id, + claim, + })); + + for (const node of claimNodes) { + const linkedSources = node.claim.sources.map((r) => ({ ...r, source: sourceById(r.sourceId) })).filter((x) => x.source); + linkedSources.forEach((entry, idx) => { + const sourceX = node.x - 90 + idx * 120; + const sourceY = 300; + drawLine(el.graph, node.x, node.y + node.r, sourceX, sourceY - 16, entry.relation); + drawNode(el.graph, { + x: sourceX, + y: sourceY, + r: 16, + label: `S${idx + 1}`, + nodeClass: "source", + id: `${node.id}-${entry.source.id}`, + onClick: () => { + state.selectedClaimId = node.id; + renderAll(); + }, + }); + }); + } + + for (const node of claimNodes) { + drawNode(el.graph, { + x: node.x, + y: node.y, + r: node.r, + label: node.id, + nodeClass: "claim", + id: node.id, + onClick: () => { + state.selectedClaimId = node.id; + setHashForClaim(node.id); + renderAll(); + }, + }); + } +} + +function trustChecks() { + const missingRequired = claims.filter((claim) => { + const hasSources = claim.sources?.length > 0; + const hasConfidence = Number.isInteger(claim.confidence); + const hasWhy = Boolean(claim.whyConfidence); + return !(hasSources && hasConfidence && hasWhy); + }); + + const brokenLinkLike = sourceInventory.filter((s) => { + try { + new URL(s.url); + return false; + } catch { + return true; + } + }); + + const duplicates = []; + const seen = new Map(); + const normalizeUrlKey = (rawUrl) => { + try { + const parsed = new URL(rawUrl); + const host = parsed.hostname.replace(/^www\./i, "").toLowerCase(); + const path = parsed.pathname.toLowerCase().replace(/\/+$/, ""); + return `${host}${path}`; + } catch { + return rawUrl.toLowerCase().replace(/\/+$/, ""); + } + }; + for (const src of sourceInventory) { + const key = normalizeUrlKey(src.url); + if (seen.has(key)) { + duplicates.push([seen.get(key), src.id]); + } else { + seen.set(key, src.id); + } + } + + const manualReview = claims.filter((c) => c.needsManualReview); + + return { missingRequired, brokenLinkLike, duplicates, manualReview }; +} + +function renderTrustAndLaunch() { + const checks = trustChecks(); + const checklist = [ + { + ok: checks.missingRequired.length === 0, + text: `Provenance completeness checks (${checks.missingRequired.length} missing)`, + }, + { + ok: checks.brokenLinkLike.length === 0, + text: `Broken-link format checks (${checks.brokenLinkLike.length} invalid)`, + }, + { + ok: checks.duplicates.length === 0, + text: `Duplicate source detection (${checks.duplicates.length} duplicate pairs)`, + }, + { + ok: checks.manualReview.length === 0, + text: `Manual review queue (${checks.manualReview.length} ambiguous claims)`, + }, + ]; + + el.trustList.innerHTML = checklist + .map((item) => `
    • ${item.ok ? "✓" : "•"} ${item.text}
    • `) + .join(""); + + el.launchChecklist.innerHTML = launchChecklist.map((item) => `
    • ✓ ${item}
    • `).join(""); +} + +function renderAll() { + renderClaims(); + renderClaimDetail(); + renderGraph(); + renderTrustAndLaunch(); +} + +function bindEvents() { + el.topic.addEventListener("change", () => { + state.topic = el.topic.value; + renderAll(); + }); + el.confidence.addEventListener("change", () => { + state.minConfidence = Number(el.confidence.value); + renderAll(); + }); + el.sourceType.addEventListener("change", () => { + state.sourceType = el.sourceType.value; + renderAll(); + }); + el.recency.addEventListener("change", () => { + state.recency = el.recency.value; + renderAll(); + }); +} + +function initFromHash() { + const fromHash = claimFromHash(); + if (fromHash && claims.some((c) => c.id === fromHash)) { + state.selectedClaimId = fromHash; + } +} + +function addContextToDetail() { + const note = document.createElement("p"); + note.innerHTML = `Audience: ${audiences.join(", ")}
      Success criteria: ${successCriteria.join( + "; ", + )}
      Source policy: ${sourcePolicy.scope}; required per claim: ${sourcePolicy.requiredPerClaim.join(", ")}.`; + el.claimDetail.prepend(note); +} + +function renderPhases() { + const phaseText = document.createElement("p"); + phaseText.innerHTML = `Delivery phases: ${[ + "1) Seeded browser graph prototype", + "2) Automated ingestion and normalization", + "3) Confidence analytics and sharing polish", + ].join(" → ")}.`; + document.querySelector(".app-header").appendChild(phaseText); +} + +populateFilterOptions(); +bindEvents(); +initFromHash(); +renderAll(); +addContextToDetail(); +renderPhases(); diff --git a/src/data.js b/src/data.js new file mode 100644 index 0000000..008c971 --- /dev/null +++ b/src/data.js @@ -0,0 +1,165 @@ +export const sourcePolicy = { + scope: "Public, non-classified, citable sources only", + requiredPerClaim: ["sourceLinks", "retrievalDate", "confidence"], +}; + +export const audiences = [ + "Curious public", + "Students", + "Educators", + "Space-policy and mission enthusiasts", +]; + +export const successCriteria = [ + "Users can trace any claim to at least one source in 1-2 clicks", + "Users can compare supporting and contradicting evidence side-by-side", + "Every displayed claim shows confidence and confidence rationale", + "All references include retrieval date and source type", +]; + +export const phases = [ + { + id: "phase-1", + title: "Phase 1: Seeded prototype", + outcome: "Static dataset + interactive browser graph", + }, + { + id: "phase-2", + title: "Phase 2: Ingestion automation", + outcome: "Automated fetch and normalization for selected public sources", + }, + { + id: "phase-3", + title: "Phase 3: Trust analytics and sharing polish", + outcome: "Contradiction detection, confidence analytics, deep-link polish", + }, +]; + +export const launchChecklist = [ + "Public hosting configured", + "Lightweight onboarding present", + "Citation/export view available", + "Mission-truth disclaimer visible in app header", +]; + +export const sourceInventory = [ + { + id: "s-nasa-artemis", + title: "NASA Artemis Campaign Overview", + type: "NASA page", + url: "https://www.nasa.gov/artemis/", + retrievedAt: "2026-03-20", + reliability: "high", + cadence: "periodic updates", + access: "public web", + topic: "program", + }, + { + id: "s-nasa-artemis2", + title: "NASA Artemis II Mission Page", + type: "NASA page", + url: "https://www.nasa.gov/mission/artemis-ii/", + retrievedAt: "2026-03-20", + reliability: "high", + cadence: "periodic updates", + access: "public web", + topic: "mission", + }, + { + id: "s-nasa-orion", + title: "NASA Orion Spacecraft", + type: "NASA page", + url: "https://www.nasa.gov/spacecraft/orion/", + retrievedAt: "2026-03-20", + reliability: "high", + cadence: "periodic updates", + access: "public web", + topic: "spacecraft", + }, + { + id: "s-esa-esm", + title: "ESA European Service Module", + type: "Agency page", + url: "https://www.esa.int/Science_Exploration/Human_and_Robotic_Exploration/Orion", + retrievedAt: "2026-03-22", + reliability: "medium", + cadence: "periodic updates", + access: "public web", + topic: "spacecraft", + }, + { + id: "s-press-release", + title: "NASA Artemis II Crew Announcement", + type: "Press release", + url: "https://www.nasa.gov/news-release/", + retrievedAt: "2026-03-22", + reliability: "medium", + cadence: "event-driven", + access: "public web", + topic: "crew", + }, + { + id: "s-wikipedia-contrast", + title: "Artemis 2 - Wikipedia", + purpose: "Used as a lower-reliability contrast source for contradiction checks.", + type: "Community summary", + url: "https://en.wikipedia.org/wiki/Artemis_2", + retrievedAt: "2026-03-22", + reliability: "low", + cadence: "continuous edits", + access: "public web", + topic: "mission", + }, +]; + +export const elements = [ + { id: "e-crew", label: "Crew", topic: "crew" }, + { id: "e-orion", label: "Orion", topic: "spacecraft" }, + { id: "e-mission-profile", label: "Mission Profile", topic: "mission" }, + { id: "e-program", label: "Artemis Program", topic: "program" }, +]; + +export const claims = [ + { + id: "c-001", + elementId: "e-crew", + topic: "crew", + statement: "Artemis II is planned as the first crewed Artemis mission around the Moon.", + confidence: 3, + whyConfidence: + "Supported by multiple official mission pages with consistent language; minor wording differences across updates.", + sources: [ + { sourceId: "s-nasa-artemis2", relation: "supports" }, + { sourceId: "s-nasa-artemis", relation: "supports" }, + { sourceId: "s-wikipedia-contrast", relation: "supports" }, + ], + needsManualReview: false, + }, + { + id: "c-002", + elementId: "e-orion", + topic: "spacecraft", + statement: "Orion is the primary crew transport spacecraft for Artemis II.", + confidence: 3, + whyConfidence: "Directly described in official NASA and ESA Orion materials.", + sources: [ + { sourceId: "s-nasa-orion", relation: "supports" }, + { sourceId: "s-esa-esm", relation: "supports" }, + ], + needsManualReview: false, + }, + { + id: "c-003", + elementId: "e-mission-profile", + topic: "mission", + statement: "Public summaries occasionally differ in specific flyby phrasing, requiring source-aware interpretation.", + confidence: 2, + whyConfidence: + "High-level mission intent is consistent, but wording and granularity can vary between official and secondary summaries.", + sources: [ + { sourceId: "s-nasa-artemis2", relation: "supports" }, + { sourceId: "s-wikipedia-contrast", relation: "contradicts" }, + ], + needsManualReview: true, + }, +]; diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..6eac29d --- /dev/null +++ b/styles.css @@ -0,0 +1,153 @@ +:root { + --bg: #0e1116; + --panel: #171d27; + --text: #e6edf3; + --muted: #9db1c9; + --accent: #5bc0ff; + --good: #7ee787; + --warn: #f2cc60; + --bad: #ff7b72; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: Inter, Segoe UI, Roboto, Arial, sans-serif; + color: var(--text); + background: var(--bg); +} + +.app-header { + padding: 1rem 1.25rem; + border-bottom: 1px solid #263041; +} + +h1 { + margin: 0 0 0.3rem; + font-size: 1.35rem; +} + +h2 { + margin-top: 0; + font-size: 1.05rem; +} + +.subtitle, +.disclaimer { + margin: 0.15rem 0; + color: var(--muted); +} + +.layout { + display: grid; + gap: 0.9rem; + padding: 0.9rem; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); +} + +.panel { + background: var(--panel); + border: 1px solid #273244; + border-radius: 10px; + padding: 0.9rem; +} + +label { + display: block; + margin-bottom: 0.6rem; + color: var(--muted); +} + +select { + width: 100%; + margin-top: 0.3rem; + padding: 0.45rem; + background: #10161f; + border: 1px solid #33435c; + color: var(--text); + border-radius: 8px; +} + +#graph { + width: 100%; + height: auto; + border-radius: 8px; + background: #0a0f16; + border: 1px solid #2b3a50; +} + +.claims-list { + display: grid; + gap: 0.55rem; +} + +.claim-button { + width: 100%; + text-align: left; + padding: 0.55rem; + border: 1px solid #32425a; + border-radius: 8px; + background: #111827; + color: var(--text); + cursor: pointer; +} + +.claim-button.active { + border-color: var(--accent); + box-shadow: 0 0 0 1px var(--accent) inset; +} + +.badge { + display: inline-block; + font-size: 0.76rem; + padding: 0.14rem 0.4rem; + border-radius: 999px; + margin-right: 0.35rem; +} + +.badge.high { + background: color-mix(in oklab, var(--good) 24%, transparent); + color: var(--good); +} + +.badge.medium { + background: color-mix(in oklab, var(--warn) 22%, transparent); + color: var(--warn); +} + +.badge.low { + background: color-mix(in oklab, var(--bad) 20%, transparent); + color: var(--bad); +} + +.source-card { + border: 1px solid #30405a; + border-radius: 8px; + padding: 0.55rem; + margin-top: 0.45rem; + background: #0f1520; +} + +.source-card.contradict { + border-color: #734246; +} + +a { + color: var(--accent); +} + +ul { + padding-left: 1.05rem; + margin-bottom: 0; +} + +li.ok { + color: var(--good); +} + +li.warn { + color: var(--warn); +}