Skip to content

test: add ClassifyContentAsyncTests — cover all 3 branches (null response, exception, type-switch)#175

Open
davidortinau wants to merge 29 commits intomainfrom
op-test-classify-content-async
Open

test: add ClassifyContentAsyncTests — cover all 3 branches (null response, exception, type-switch)#175
davidortinau wants to merge 29 commits intomainfrom
op-test-classify-content-async

Conversation

@davidortinau
Copy link
Copy Markdown
Owner

What

Adds ClassifyContentAsyncTests.cs — 6 xUnit tests that directly exercise every code path in ContentImportService.ClassifyContentAsync (ContentImportService.cs:870), which was previously untested. Existing tests mock away classification by pre-setting ContentType on the request, so the mapping and fallback logic had zero coverage.

Also adds [InternalsVisibleTo("SentenceStudio.UnitTests")] via a new AssemblyInfo.cs in the Shared project to expose the internal ContentClassificationAiResponse DTO to the test project.

Why

ClassifyContentAsync is the hot auto-detect path on the feature/import-content branch. Without direct tests, regressions in any of its three branches (null AI response, AI exception, type-switch mapping) would only surface as silent wrong behaviour in integration.

Tests added

Test Branch covered
ClassifyContent_NullAiResponse_DefaultsToVocabulary Null response → Vocabulary / 0.5f confidence
ClassifyContent_AiException_DefaultsWithLowConfidence Exception → Vocabulary / 0.3f + "error" signal
ClassifyContent_KnownType_MapsCorrectly (Theory ×4) vocabulary, phrases, sentences, transcript arms
ClassifyContent_UnknownType_FallsBackToVocabulary Wildcard _ arm → Vocabulary

Verification

dotnet test tests/SentenceStudio.UnitTests/
Passed! Failed: 0, Passed: 495, Skipped: 0, Total: 495

489 pre-existing tests all still pass.

davidortinau and others added 29 commits April 24, 2026 10:57
- Orchestration log: DX24 vocab crash fix workflow (commit c9b1d0a)
- Session log: ExposureCount NULL→0 backfill, verified on device
- Decision merge: inbox → decisions.md (3 entries: lexical patch, validation strategy, directive)
- Deleted: inbox files + jayne-locale-screenshots (deduped)
- Wash history: append CRITICAL RULE for SQLite migration defense-in-depth (DEFAULT + idempotent backfill)
- No archival needed (no entries >30d old in decisions.md)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Orchestrated multi-agent session for new generic data import feature architecture:

- Zoe (Lead): Full MVP architecture covering UX flows, content detection (heuristic-first,
  AI fallback), data model (no new tables), parsing pipeline, AI integration, service layer,
  scope phasing. Placement revision: separate `/import-content` page (not tab on existing
  `/import`), per Captain directive.

- Wash (Data Layer Scout): Surveyed existing YouTube pipeline pattern, found file import
  UI/service gap, confirmed no schema changes needed, identified dedup inconsistency for
  standardization.

- River (AI Strategy): Designed 5 AI prompt tasks with confidence thresholds, heuristic
  fallbacks, structured DTOs via `SendPrompt<T>`, permissive grading principles.

- Kaylee (UI Scout): Documented form patterns, file picker abstractions, resource lookup
  UI, navigation placement. Recommendations for ContentImportService, InputFile, preview
  table reuse.

- Copilot Directive: Confirmed scope separation — Video Subscriptions (existing) separate
  from generic content import (new) to keep features focused and growth paths independent.

Artifacts:
- 5 orchestration logs (one per agent) in .squad/orchestration-log/
- Session log in .squad/log/2026-04-24T22:31-data-import-architecture-plan.md
- 6 decisions merged into .squad/decisions.md (168KB total)
- All agent histories updated with import-plan context

Next: Implementation team begins ContentImportService + ImportContent.razor page. River
engineers 3 prompt templates (Format Inference, Content Classification, Extraction).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Captain resolved 7 open questions from architecture proposal:

1. Dedup default: Skip and reuse existing (safest, matches YouTube pipeline)
2. Source attribution: Deferred to v2
3. Single-column imports: Use AI to translate on import (new mvp-single-column-translate work item)
4. Mobile clipboard paste: Not needed for MVP
5. Replace-all mode: Deferred to v2
6. Renamed YouTube page: 'Media Import' at /media-import (not 'Video Subscriptions')
7. Separate-page placement: Confirmed — /import-content independent from /media-import

Updated decisions.md with final rulings appended and prior superseded mentions annotated.
All prior architecture content preserved; only updated with Captain's decisions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t rename + FreeTextToVocab prompts

- Add ContentImportService (Shared/Services) with locked DTO surface, full transaction-safe CommitImportAsync, scoped DI
- Rename Import.razor → MediaImport.razor, route /import → /media-import (back-compat preserved on both routes)
- Add FreeTextToVocab.scriban-txt + TranslateMissingNativeTerms.scriban-txt prompts and response DTOs
- Standardized dedup rule: case-sensitive, whitespace-trimmed on TargetLanguageTerm
- Captain's rulings 2026-04-24 applied throughout

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ContentImportService.ParseContentAsync now real: CSV/TSV/pipe sniffing, JSON parse, free-text AI fallback (50KB cap), single-column AI translation
- Add ImportContent.razor at /import-content — 7-step wizard (paste → preview → target → dedup → commit) with editable preview table, badge-marked AI-translated rows, new/existing resource picker, dedup mode selector
- +43 localization keys (English + Korean) for import flow
- Dedup audit complete: VideoImportPipelineService inconsistency documented for Captain
- Cleanup: stage delete of legacy Import.razor missed by Wave 1 commit

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…E script

- Extract IAiService interface; dual DI registration (concrete + interface alias) keeps 16 existing consumers untouched
- Migrate ContentImportService constructor to IAiService dependency
- Add ContentImportServiceTests.cs — 18 unit tests, 0 failures, ~95% coverage of public API
  • Format detection (CSV/TSV/pipe/JSON), header rows, delimiter override
  • Two-column happy path, empty-row error, single-column AI translation, free-text AI fallback, 50KB cap
  • Whitespace-trimmed + case-sensitive dedup
  • CommitImportAsync: Skip / Update / ImportAll / new-resource / existing-resource paths
  • NotSupportedException for v2 stubs (Phrases, Transcript)
- Add 15-scenario E2E script at .claude/skills/e2e-testing/references/import-content.md
- 0 bugs discovered — implementation shipshape

MVP COMPLETE: 11/11 todos shipped on feature/import-content-mvp.
v2 backlog: phrases/transcript content types, AI auto-detect with confidence, ImportHistory + undo.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Code-review findings on feature/import-content-mvp before push:

1. CommitImportAsync could create duplicate VocabularyWord rows when a
   single import contained multiple rows with the same trimmed
   TargetLanguageTerm. EF's FirstOrDefaultAsync queries the database and
   does not see tracked-but-unsaved entities, so each row in the loop
   minted a fresh word. Added an in-batch Dictionary cache keyed by
   trimmed target term that:
     - Skip mode reuses the in-flight word (counts as skipped)
     - Update mode applies last-non-empty native term (last write wins)
     - ImportAll bypasses the cache so duplicates remain intentional
   Two regression tests added (Skip + ImportAll variants).

2. ImportContent.razor preview never rendered the row.IsAiTranslated
   flag despite the E2E script and Captain's ruling #3 requiring a
   visible badge. Added a Bootstrap bi-stars badge in the
   NativeLanguageTerm cell with EN + KO localization keys
   (Import_AiTranslated_Badge / _Tooltip).

Tests: 20/20 ContentImportServiceTests passing (was 18, +2 new).
UI build: 0 errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bugs found while walking the import-content wizard end-to-end via Playwright:

1. NavigationMemoryService missing 'import-content' route
   - NavMenu.razor TopItems contained the 'import-content' key, but
     NavigationMemoryService.Sections array didn't have a matching entry.
   - GetSectionRoute() silently returned '/' (Array.Find default tuple),
     so clicking the sidebar 'Import Content' button quietly redirected
     users to the dashboard with no error.
   - Added ('import-content', '/import-content', ['/import-content']).

2. Paste textarea Preview button stuck disabled
   - <textarea @Bind="rawText"> defaulted to the 'change' event, which
     only fires on blur. Users who pasted/typed and immediately clicked
     Preview saw a disabled button until they clicked outside.
   - Added @Bind:event="oninput" so rawText updates on every keystroke.

3. AI translation badge never rendered
   - ContentImportService correctly set ImportRow.IsAiTranslated=true
     for AI-translated rows, but the editableRows projection in
     ParseContent() copied every property EXCEPT IsAiTranslated, so the
     bi-stars 'AI' badge rendered for nobody.
   - Added IsAiTranslated to the projection.

E2E verified end-to-end against Aspire-hosted Postgres webapp:
- 5-row CSV happy path: 1 LearningResource + 5 VocabularyWord + 5 mappings
  persisted in Postgres (verified via psql).
- Intra-batch dedup: 5 input rows with duplicates → 3 unique mappings,
  reusing pre-existing word rows for already-known terms.
- Single-column AI: 사과/바나나/포도 → apple/banana/grape with bi-stars
  'AI' badge visible on each row.

20/20 ContentImportService tests still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add scope-discipline rule (no silent deferral of named features)
- Capture 4 autonomous decisions for import v1.1
- Land Zoe's v1.1 architecture spec
- Correct Decision #1: use existing LexicalUnitType enum
- Status: BLOCKED on captain-confirm-scope

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ipt/auto-detect branches

Migration SetDefaultLexicalUnitType:
- Heuristic backfill: Unknown→Word (no space) or Phrase (has space)
- Dual-provider: Postgres POSITION, SQLite INSTR
- Down() is safe no-op (no data loss)

ContentImportService v1.1:
- Phrase branch: harvests Words + Phrases via FreeTextToVocab prompt
- Transcript branch: stores full text + word-biased extraction
- Auto-detect branch: three-tier confidence gate (>=0.85/0.70/below)
- Classification runs BEFORE any DB persistence (Captain directive)
- Checkbox harvest model: HarvestTranscript/Phrases/Words booleans
- ImportRow gains LexicalUnitType for per-row classification
- Zero-vocab: persist resource + warning (no silent success)
- Transcript >30KB: reject with clear error (chunking in v1.2)

UI: adapted DetectContentType → ClassifyContentAsync (async)

Blockers on River: ClassifyImportContent + ExtractPhrasesFromContent
prompts not yet landed — using bridge implementations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Archive decisions.md (229KB), merge 10 inbox decisions (River prompts,
Wash backend, Kaylee UI, Jayne tests + phrase gap, 5 Captain confirms),
write 4 orchestration logs, session log, update 5 agent histories.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cisions merge

This commit logs the complete v1.1 Data Import cycle from initial DO-NOT-SHIP verdict
through bug fixes by Simon (backend) and Kaylee (frontend DTO mapping), retest by Jayne,
and final full sweep confirming 10/10 scenarios PASS with SHIP verdict.

Artifacts:
- Orchestration logs: 4 agents × 4 spans (simon, jayne-retest, kaylee-mapping-fix, jayne-full-sweep)
- Session log: 2026-04-26T23:23:07Z-v11-import-ship.md (cycle summary from rejection to SHIP)
- Decisions merge: All 5 v1.1 inbox files appended to .squad/decisions.md (inbox cleaned)
- Agent history updates: simon, wash (lockout), kaylee (DTO discipline), jayne (dual-layer verification), river (prompts clean)

Key learnings captured:
- Simon: UserProfileId scoping, transcript DTO carry-through, LLM mapping discipline
- Kaylee: DTO mapping completeness audit pattern
- Jayne: Frontend DTO bugs masquerade as backend bugs; dual-layer verification required
- River: Prompt engineering discipline validated; C# layer had the bugs
- Wash: Lockout scoped to 3 specific bugs; resolution delivered; learnings documented for future

Bug fixes verified with DB-level evidence across 10 regression scenarios.
All P1/P0 bugs confirmed fixed; zero regressions; clean Aspire logs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…+ Phrases/Transcript/Auto

Three production-blocking bugs from e2e:

BUG-1 (P0, UserProfileId leak): ContentImportService bypassed
LearningResourceRepository.SaveAsync, writing raw to DbContext and
leaving UserProfileId null. Inject IPreferencesService and source
ActiveUserId on the service path so new resources are owned by the
active profile (mirrors existing repository pattern).

BUG-2 (P1, SourceText drop): Commit path discarded preview source
text when TranscriptText was unset. Add SourceText to
ContentImportPreview, fall back commit.TranscriptText ??
commit.Preview.SourceText, and pass the field through the Razor
preview→commit DTO mapping.

BUG-3 (P1, LexicalUnitType lost): ParseFreeTextContentAsync never
mapped item.LexicalUnitType from the AI response — every row defaulted
to Word silently, including obvious phrases. Wire explicit mapping in
all three row-creation paths (free-text, transcript, CSV) and add
ResolveLexicalUnitType heuristic fallback (multi-word term → Phrase).
ImportContent.razor also re-mapped LexicalUnitType in ImportRow→preview.

Adds the v1.1 content-type prompts:
- ClassifyImportContent.scriban-txt — auto-detect classifier
- ExtractVocabularyFromPhrases.scriban-txt — phrase + constituent words
- ExtractVocabularyFromTranscript.scriban-txt — word-bias rework

Verification: 10/10 e2e scenarios PASS on Mac Catalyst (David, ko-KR).
0 cross-user UserProfileId leaks. SourceText round-trips at 441/219 chars.
LexicalUnitType: 0 Unknown, 213 Word, 6 Phrase. Aspire logs clean.

Known follow-ups (not blocking ship):
- BUG-4 (P2): AI confidence calibration never returns <85%
- New-resource save isn't wrapped in a transaction (low-prob orphan)
- Korean localization gap on harvest checkbox section
- Silent title validation
- Classification / RequiresUserConfirmation DTO fields unconsumed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add LexicalUnitType filter dropdown (All/Word/Phrase/Sentence) to
  both desktop filter row and mobile offcanvas panel
- Rename 'Add Word' button to 'Add' (new Vocabulary_Add localization key)
- Add type filter case in ApplyFilters (client-side, no new queries)
- Add type entries in filter badge/icon helpers
- Add localization keys for en + ko

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Part A: ExtractVocabularyFromPhrases.scriban-txt
- Root cause: service wires to FreeTextToVocab instead of this prompt (Wash fix)
- Added harvest_phrases/harvest_words Scriban conditionals
- Added pipe-delimited input handling
- Added CRITICAL rule: each input line MUST produce a Phrase entry
- Added Captain's exact Korean|English few-shot example

Part B: ExtractVocabularyFromSentences.scriban-txt (NEW)
- Sentence as first-class content type (LexicalUnitType.Sentence)
- Three harvest flags: harvest_sentences, harvest_phrases, harvest_words
- Sentences preserve original text verbatim (no dictionary normalization)
- Three few-shot examples with Korean|English

Part C: ClassifyImportContent.scriban-txt
- Added Sentences as fourth classification type
- Heuristic: sentence-completeness test (subject+predicate+punctuation)
- Sentences vs Phrases: full utterances vs sub-sentence fragments
- Added borderline-case guidance and two new few-shot examples

Part D: Coordination doc for Wash in decisions inbox

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause: Phrases branch called ParseFreeTextContentAsync (generic
FreeTextToVocab prompt) which decomposed input into individual words,
silently dropping phrase/sentence entries. River's dedicated
ExtractVocabularyFromPhrases.scriban-txt was deployed but never wired in.

Fix:
- Rewrote Phrases/Sentences branch with two-step pipeline:
  1. Parse delimited lines for primary phrase/sentence entries
  2. Run AI phrase extraction for constituent words
  3. Combine with dedup, filter by harvest flags
- Added ContentType.Sentences enum value
- Added HarvestSentences boolean on ContentImportRequest/Commit DTOs
- Refined ResolveLexicalUnitType: terminal punctuation -> Sentence,
  whitespace only -> Phrase (replaces blanket space->Phrase heuristic)
- Updated classifier prompt to recognize Sentences as fourth type
- Updated stale test (Phrases no longer throws NotSupportedException)
- No schema migration needed (LexicalUnitType.Sentence already exists)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Round 1 complete — three-agent convergent diagnosis of phrase-save bug root cause.

ROOT CAUSE: ContentImportService.cs line 192 routed Phrases branch to generic
ParseFreeTextContentAsync() instead of River's dedicated ExtractVocabularyFromPhrases
prompt (which was written but never wired in — TODO comment at line 191).

FIXES:
- Wash: Rewrote Phrases as 2-step pipeline (parse delimited → preserve phrase entries
  → AI harvest constituent words → dedup). Added ContentType.Sentences + HarvestSentences
  flag. Refined ResolveLexicalUnitType heuristic (terminal punctuation → Sentence).
  Build green, 610 tests passing.
- River: Fixed ExtractVocabularyFromPhrases prompt (harvest flags, pipe handling).
  Created NEW ExtractVocabularyFromSentences prompt. Updated ClassifyImportContent to
  four-class output. Locked JSON response shape (identical across all 3 prompts).
- Kaylee: Added Type filter dropdown to Vocabulary page (Round 1). Round 2 pending:
  Sentences button + harvest checkbox in import UI.
- Jayne: Reproduced bug (3 Korean|English in → 8 words, 0 phrases). Created 7-section
  test plan for Round 2 validation.

FILES:
- .squad/orchestration-log/2026-04-27T0330Z-{wash,river,kaylee,jayne}.md — per-agent logs
- .squad/log/2026-04-27T0330Z-v12-import-bug-fix.md — session summary
- .squad/decisions.md — merged 4 inbox decisions (delete inbox files after)
- .squad/agents/{wash,river,kaylee,jayne}/history.md — team convergence notes

No schema migration needed (LexicalUnitType.Sentence already exists).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nto UI

- Add Sentences option to content type dropdown (between Phrases and Transcript)
- Add Harvest Sentences checkbox (order: Sentences > Phrases > Words)
- Add Sentences button to auto-detect type chooser and override panel
- Wire HarvestSentences into ContentImportRequest (preview) and ContentImportCommit
- Extend harvest validation to include harvestSentences in at-least-one check
- Add Sentences case to FormatContentType display helper
- Defaults: Sentences type sets Sentences=true, Words=true, Phrases=false
- Reset harvestSentences in StartNewImport

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
S2 (primary): ParseContentAsync always passed LexicalUnitType.Phrase as
the hint to ResolveLexicalUnitType regardless of content type. With
Sentences harvest defaults (harvestPhrases=false), primary rows were
classified Phrase then filtered out — only AI Word rows survived.

Fix: pass LexicalUnitType.Sentence hint when effectiveContentType is
Sentences. Also reorder ResolveLexicalUnitType to check single-token
BEFORE trusting the Phrase/Sentence hint (single tokens stay Word per
Captain's directive).

S1 (secondary): AI extraction always used the Phrases prompt. Now
branches to ExtractVocabularyFromSentencesAsync which loads River's
ExtractVocabularyFromSentences.scriban-txt and passes harvest flags.

4 new tests verify: sentences with punctuation, sentences without
punctuation, single-token stays Word, phrases regression guard.
All 24 ContentImportService tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…+ decision merge + history updates

Rounds 3-4 of v1.2 import bug fix cycle completed:
- Round 3 (Jayne): E2E validation identified Sentences producing 0 type=3 rows
- Root cause investigation (Wash): Two bugs found (content-type-unaware hint + unwired Sentences AI prompt)
- Round 4 (Jayne): Re-verification confirmed fix working (Test 1 regression guard PASS, Test 2 fix PASS)

Files:
- Added orchestration logs: jayne-r3, wash-r3, jayne-r4
- Added session log: v12-sentences-branch-fix
- Merged decision inbox into decisions.md (wash-sentences-branch-fix.md)
- Deleted inbox file
- Updated agent history (Wash + Jayne) with R3-R4 entries

DB verification: Word +7, Phrase +5, Sentence +4 (was 0, now working)
Commit under test: 3c7a4cc

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add ImportItemStatus enum (Created/Updated/Skipped/Failed) and
ContentImportItemResult class with VocabularyWordId, Lemma,
NativeLanguageTerm, Type, Status, and curated Reason fields.

Every branch in CommitImportAsync that increments an aggregate count
now also appends a detail item to the new Items list. Failed rows
log via _logger.LogError with structured fields for Aspire retrieval.

Invariant: Items.Count == CreatedCount + UpdatedCount + SkippedCount + FailedCount

8 new unit tests (32 total ContentImportService tests passing).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add per-row results table below the 4-card summary showing Lemma,
  Native Term, Type badge, Status pill, and Reason for each imported row
- Add filter pills (All/Created/Updated/Skipped/Failed) for client-side
  filtering of the results table
- Clickable rows link to /vocabulary/edit/{id} for Created/Updated/Skipped
  items; Failed rows are not clickable and show curated failure reason
- Mobile responsive: stacked card layout on narrow viewports, table on desktop
- Add IImportResultStore service (Singleton, 30-min TTL) for state
  preservation across back-navigation via URL query param ?completed={guid}
- Register ImportResultStore in BlazorUIServiceExtensions.cs
- Add 8 new localization keys (en + ko) for table headers, filter pills,
  and view link text
- Add blazor-nav-state-preservation skill to .squad/skills/

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Scribe: orchestration logs for Wash, Kaylee, Jayne
- orchestration-log/2026-04-27T14:53:00Z-wash.md — backend DTOs + tests
- orchestration-log/2026-04-27T14:53:00Z-kaylee.md — UI redesign + state mgmt
- orchestration-log/2026-04-27T14:53:00Z-jayne.md — E2E validation (7/7 PASS)

Session log:
- log/2026-04-27T14:53:00Z-v13-import-detail.md — cycle summary + ship gate

Decision inbox merged & archived:
- Merged: wash-import-item-result.md (ContentImportResult DTO design)
- Merged: kaylee-import-result-store.md (singleton store + TTL strategy)
- Merged: jayne-v13-import-detail-verdict.md (E2E test results)

Cross-agent updates:
- wash/history.md: appended Jayne verdct note
- kaylee/history.md: appended Jayne verdict note

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add IsDuplicate (bool) and DuplicateReason (string?) to ImportRow DTO.
New EnrichPreviewWithDuplicateInfoAsync method uses a single batched DB
query to flag rows that already exist in VocabularyWord table, plus
intra-batch duplicate detection for repeated terms within the same preview.

Extract NormalizeTargetTerm() as the shared matching predicate used by
both preview enrichment and CommitImportAsync — single source of truth
for the trimmed, case-sensitive ordinal comparison.

Reason keys: AlreadyInVocabulary, DuplicateWithinBatch.

4 new tests (36 total ContentImportService tests passing):
- Exact duplicate flagged correctly
- Near-miss (different lemma) not flagged
- Batch query structure (no N+1)
- Round-trip invariant: Preview IsDuplicate matches Commit Skip/Create

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Documented the style normalization patterns applied to ImportContent.razor:
- Badge type system: bg-{color} bg-opacity-10 text-{color}
- Clickable elements: role="button" not inline cursor
- No bespoke hex colors in Razor markup
- Duplicate badge column patterns (coordinated with Wash)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…azor

Wash's backend enrichment method was defined and tested but never called
from the Razor code-behind. Preview rows were always showing IsDuplicate=false.

Added the missing call between ParseContentAsync and row mapping, which is
the integration point Wash's decision doc specified.

Found by Jayne during E2E validation of commit 5d98a27.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Merged wash-preview-duplicate-flag.md (EnrichPreviewWithDuplicateInfoAsync contract)
- Merged kaylee-import-style-cleanup.md (style audit + badge pattern)
- Captured integration gap learning in Jayne's history
- Deleted merged inbox files
- Updated Scribe history with v1.4 cycle summary

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
 Vocabulary / 0.5f confidence
 Vocabulary / 0.3f confidence with error signals
- 4-arm type-switch (vocabulary, phrases, sentences, transcript)
 Vocabulary fallback

Also adds [InternalsVisibleTo("SentenceStudio.UnitTests")] via
src/SentenceStudio.Shared/AssemblyInfo.cs to expose the internal
ContentClassificationAiResponse DTO to the test project.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant