From 253368d77ea1839a505798ab322bc208099efd69 Mon Sep 17 00:00:00 2001 From: jon-devlapaz Date: Mon, 13 Jul 2026 16:10:30 -0500 Subject: [PATCH] feat(library): flatten evidence index Library previously rendered learner concepts as promotional cards with source and map metadata, obscuring learner-owned reconstruction. Replace that chrome with a divided evidence index so Library stays aligned with Generation Before Recognition and graph truth. Cache-bust the changed frontend assets. No deployment workaround is required. Agent-Role: Codex --- public/css/index.css | 2 +- public/css/paper.css | 143 +++++++++++++++++++++- public/index.html | 4 +- public/js/app.js | 2 +- public/js/library-view.js | 26 +--- tests/e2e/test_app_helper_modules.py | 14 +-- tests/e2e/test_smoke.py | 26 +++- tests/test_frontend_app_helper_modules.py | 22 +--- 8 files changed, 176 insertions(+), 63 deletions(-) diff --git a/public/css/index.css b/public/css/index.css index 6b61504..5eaa502 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -21,4 +21,4 @@ @import '../styles.css?v=171' layer(components); @import '../antigravity.css?v=40' layer(legacy); -@import 'paper.css?v=19' layer(paper); +@import 'paper.css?v=20' layer(paper); diff --git a/public/css/paper.css b/public/css/paper.css index dbdc141..55e255a 100644 --- a/public/css/paper.css +++ b/public/css/paper.css @@ -757,8 +757,8 @@ /* ── Library index ─────────────────────────────────────────── The Library is an index of learner-owned concepts, not a promotional - empty-state card. Keep the first-use path compact and leave the existing - populated evidence cards untouched. */ + card grid. Keep first use compact; populated Library is a quiet, divided + list that foregrounds learner reconstruction evidence. */ body.antigravity-theme #library-view { min-height: calc(100dvh - 76px); @@ -771,7 +771,7 @@ body.antigravity-theme #library-view { } .library-shell { - width: min(100%, 68rem); + width: min(100%, 56rem); margin: 0 auto; } @@ -910,7 +910,117 @@ body.antigravity-theme #library-view { } .library-vault-grid { - margin-top: var(--space-5); + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 0; + margin-top: 0; +} + +.library-card.library-card-vault { + display: block; + min-width: 0; + min-height: 0; + margin: 0; + padding: var(--space-5) var(--space-1); + overflow: visible; + border: 0; + border-bottom: 1px solid var(--border-subtle); + border-radius: 0; + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + text-align: left; + transition: background-color var(--duration-micro) var(--ease-standard); +} + +.library-card.library-card-vault::after { + content: none; +} + +.library-card.library-card-vault:hover { + transform: none; + border-color: var(--border-subtle); + background: var(--accent-soft); + box-shadow: none; +} + +.library-card.library-card-vault:active { + transform: none; + background: var(--accent-soft-strong); +} + +.library-card.library-card-vault:focus-visible { + position: relative; + z-index: 1; + outline: none; + box-shadow: var(--accent-ring); +} + +.library-card-header { + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: space-between; + gap: var(--space-2) var(--space-4); + margin: 0; +} + +.library-card-header > div { + flex: 1 1 10rem; + min-width: 0; +} + +.library-card-name { + display: block; + color: var(--text-strong); + font-family: var(--font-body); + font-size: var(--text-lg); + font-weight: 700; + line-height: var(--leading-snug); + letter-spacing: var(--tracking-tight); + overflow-wrap: anywhere; +} + +.library-card-state { + flex: 0 0 auto; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + color: var(--primary-readable); + font-family: var(--font-body); + font-size: var(--text-xs); + font-weight: 600; + line-height: var(--leading-snug); + letter-spacing: 0; + text-transform: lowercase; + white-space: nowrap; +} + +.library-card-state[data-state="needs repair"] { + color: color-mix(in srgb, var(--node-drilled) 50%, var(--text-strong)); + background: transparent; +} + +.library-card-state[data-state="solidified"] { + color: color-mix(in srgb, var(--node-solidified) 50%, var(--text-strong)); + background: transparent; +} + +.library-card-summary { + display: -webkit-box; + max-width: 65ch; + margin: var(--space-3) 0 0; + overflow: hidden; + color: var(--text-strong); + font-family: var(--font-body); + font-size: var(--text-sm); + line-height: var(--leading-normal); + text-wrap: pretty; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } @media (max-width: 899px) { @@ -936,11 +1046,36 @@ body.antigravity-theme #library-view { justify-self: start; padding: 0; } + + .library-card-header { + display: block; + } + + .library-card-state { + display: block; + margin-top: var(--space-1); + white-space: normal; + } +} + +@media (forced-colors: active) { + .library-card.library-card-vault:focus-visible { + outline: 2px solid Highlight; + outline-offset: -2px; + box-shadow: none; + } } /* ── Reduced motion ────────────────────────────────────────── */ @media (prefers-reduced-motion: reduce) { + .library-card.library-card-vault, + .library-card.library-card-vault:hover, + .library-card.library-card-vault:active { + transform: none; + transition: none; + } + .ig-button, .ig-button:hover:not(:disabled), .ig-button:focus-visible, diff --git a/public/index.html b/public/index.html index 4b5bc9e..5d62bd8 100644 --- a/public/index.html +++ b/public/index.html @@ -20,7 +20,7 @@ - + @@ -435,7 +435,7 @@

- + diff --git a/public/js/app.js b/public/js/app.js index eb1c030..5a34858 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -47,7 +47,7 @@ import { persistPhaseBResumeState as persistStoredPhaseBResumeState, persistPhaseBSessionState as persistStoredPhaseBSessionState, } from './phase-b-session.js'; -import { buildLibraryHtml } from './library-view.js?v=5'; +import { buildLibraryHtml } from './library-view.js?v=6'; import { createTrainingStore, TRAINING_SCHEMA_VERSION, TRAINING_STORE_KEY_PREFIX } from './training-store.js'; import { hydrateAndSyncLearnerState, diff --git a/public/js/library-view.js b/public/js/library-view.js index 55ae7cb..22c34c4 100644 --- a/public/js/library-view.js +++ b/public/js/library-view.js @@ -1,5 +1,5 @@ import { escHtml } from './html.js'; -import { deriveConceptBadge, parseConceptGraphData } from './concept-status.js'; +import { deriveConceptBadge } from './concept-status.js'; const EMPTY_RECONSTRUCTION_COPY = 'Your first reconstruction will appear here.'; @@ -44,28 +44,13 @@ export function getBestLearnerAttempt(training) { })[0]; } -export function getLibraryConceptMeta(concept, training = null) { - const graph = parseConceptGraphData(concept); - - const metadata = graph?.metadata || {}; - const clusters = Array.isArray(graph?.clusters) ? graph.clusters : []; - const subnodeCount = clusters.reduce((total, cluster) => total + ((cluster.subnodes || []).length), 0); +export function getLibraryConceptMeta(_concept, training = null) { const bestAttempt = getBestLearnerAttempt(training); const thesis = bestAttempt?.user_text || EMPTY_RECONSTRUCTION_COPY; - const sourceLabel = concept.contentFilename - ? `Source: ${concept.contentFilename}` - : concept.contentType - ? `Source: ${concept.contentType.toUpperCase()}` - : (metadata.source_title ? `Map: ${metadata.source_title}` : 'Draft map'); return { thesis: thesis.length > 180 ? `${thesis.slice(0, 177).trimEnd()}...` : thesis, summarySource: bestAttempt ? 'learner_attempt' : 'none', - architecture: metadata.architecture_type ? metadata.architecture_type.replace(/_/g, ' ') : null, - difficulty: metadata.difficulty || null, - clusterCount: clusters.length, - subnodeCount, - sourceLabel, }; } @@ -115,18 +100,11 @@ export function buildLibraryHtml(concepts, trainingByConceptId = {}, options = {
-
${escHtml(meta.sourceLabel)}
${escHtml(conceptName)}
${stateBadge}

${escHtml(meta.thesis)}

-
- ${meta.architecture ? `${escHtml(meta.architecture)}` : ''} - ${escHtml(`${meta.clusterCount} ${meta.clusterCount === 1 ? 'section' : 'sections'}`)} - ${escHtml(`${meta.subnodeCount} ${meta.subnodeCount === 1 ? 'entry' : 'entries'}`)} -
-
Open concept
`; }).join('') + ``; } diff --git a/tests/e2e/test_app_helper_modules.py b/tests/e2e/test_app_helper_modules.py index 395d305..3da7c85 100644 --- a/tests/e2e/test_app_helper_modules.py +++ b/tests/e2e/test_app_helper_modules.py @@ -475,16 +475,7 @@ def test_app_helper_modules_preserve_browser_contracts(clean_page: Page, base_ur same(library.getLibraryConceptMeta({ name: 'Concept', state: 'growing', graphData: graph }), { thesis: 'Your first reconstruction will appear here.', summarySource: 'none', - architecture: 'cause effect', - difficulty: 'medium', - clusterCount: 2, - subnodeCount: 3, - sourceLabel: 'Map: Source Title', }, 'library metadata'); - assert(library.getLibraryConceptMeta({ - contentFilename: 'notes.md', - graphData: JSON.stringify({ metadata: { core_thesis: 'From file' }, clusters: [] }), - }).sourceLabel === 'Source: notes.md', 'library filename source'); const training = { node_records: { n1: { @@ -523,8 +514,9 @@ def test_app_helper_modules_preserve_browser_contracts(clean_page: Page, base_ur assert(libraryHtml.includes('<Unsafe>'), 'library escaped name'); assert(libraryHtml.includes('Learner-owned reconstruction.'), 'library card learner evidence'); assert(!libraryHtml.includes('This is the central claim.'), 'library card no AI thesis fallback'); - assert(libraryHtml.includes('2 sections'), 'library section count'); - assert(libraryHtml.includes('3 entries'), 'library entry count'); + assert(!libraryHtml.includes('library-card-kicker'), 'library has no source kicker'); + assert(!libraryHtml.includes('library-card-meta'), 'library has no structure metadata'); + assert(!libraryHtml.includes('library-card-cta'), 'library row needs no duplicate CTA'); window.App.showLibrary(); assert(document.getElementById('library-view').classList.contains('visible'), 'library view visible'); assert(document.getElementById('library-content').textContent.includes('Your first reconstruction starts here'), 'library app path'); diff --git a/tests/e2e/test_smoke.py b/tests/e2e/test_smoke.py index be5607b..a2e4918 100644 --- a/tests/e2e/test_smoke.py +++ b/tests/e2e/test_smoke.py @@ -554,6 +554,7 @@ def test_library_card_uses_training_evidence_not_ai_summary( page: Page, base_url: str ) -> None: """Library summaries must be learner evidence, not generated source text.""" + page.set_viewport_size({"width": 390, "height": 844}) _enter_app_shell_as_guest(page, base_url) page.evaluate("localStorage.clear(); sessionStorage.clear();") _seed_training_truth_concept(page) @@ -563,7 +564,7 @@ def test_library_card_uses_training_evidence_not_ai_summary( sidebar_dot = page.locator(".concept-item", has_text="Training Truth QA").locator(".concept-dot") expect(sidebar_dot).to_have_attribute("data-state", "primed") - page.locator("#nav-library").click() + page.locator("#bn-library").click() card = page.locator(".library-card-vault", has_text="Training Truth QA") expect(card).to_be_visible() expect(card).to_have_attribute("data-state", "primed") @@ -574,6 +575,29 @@ def test_library_card_uses_training_evidence_not_ai_summary( expect(card).not_to_contain_text("growing") expect(card).not_to_contain_text("AI GENERATED CORE THESIS SHOULD NOT APPEAR") expect(card).not_to_contain_text("SOURCE PREVIEW SHOULD NOT APPEAR") + expect(card.locator(".library-card-kicker")).to_have_count(0) + expect(card.locator(".library-card-meta")).to_have_count(0) + expect(card.locator(".library-card-cta")).to_have_count(0) + + card_style = card.evaluate( + """element => { + const style = getComputedStyle(element); + return { + backgroundImage: style.backgroundImage, + borderRadius: style.borderRadius, + boxShadow: style.boxShadow, + }; + }""" + ) + assert card_style == { + "backgroundImage": "none", + "borderRadius": "0px", + "boxShadow": "none", + } + card_box = card.bounding_box() + assert card_box is not None + assert card_box["height"] < 170 + assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") def test_library_training_render_survives_one_corrupt_record( diff --git a/tests/test_frontend_app_helper_modules.py b/tests/test_frontend_app_helper_modules.py index 9a0d751..086de8f 100644 --- a/tests/test_frontend_app_helper_modules.py +++ b/tests/test_frontend_app_helper_modules.py @@ -766,20 +766,8 @@ def test_library_view_helpers_preserve_card_metadata_and_empty_state() -> None: { thesis: 'Your first reconstruction will appear here.', summarySource: 'none', - architecture: 'cause effect', - difficulty: 'medium', - clusterCount: 2, - subnodeCount: 3, - sourceLabel: 'Map: Source Title', } ); - assert.equal( - getLibraryConceptMeta({ - contentType: 'pdf', - graphData: JSON.stringify({ metadata: { thesis: 'x'.repeat(200) }, clusters: [] }), - }).sourceLabel, - 'Source: PDF' - ); assert.ok(getLibraryConceptMeta({ graphData: '{' }).thesis.includes('first reconstruction')); const training = { @@ -807,11 +795,6 @@ def test_library_view_helpers_preserve_card_metadata_and_empty_state() -> None: { thesis: 'The learner reconstructed the causal mechanism.', summarySource: 'learner_attempt', - architecture: 'cause effect', - difficulty: 'medium', - clusterCount: 2, - subnodeCount: 3, - sourceLabel: 'Map: Source Title', } ); @@ -841,8 +824,9 @@ def test_library_view_helpers_preserve_card_metadata_and_empty_state() -> None: assert.ok(cardHtml.includes('<Unsafe>')); assert.ok(cardHtml.includes('The learner reconstructed the causal mechanism.')); assert.ok(!cardHtml.includes('This is the central claim.')); - assert.ok(cardHtml.includes('2 sections')); - assert.ok(cardHtml.includes('3 entries')); + assert.ok(!cardHtml.includes('library-card-kicker')); + assert.ok(!cardHtml.includes('library-card-meta')); + assert.ok(!cardHtml.includes('library-card-cta')); const noEvidenceHtml = buildLibraryHtml([ { id: 'legacy-solid', name: 'Legacy Solid', state: 'actualized', graphData: graph },