Skip to content

Commit 01e0f3a

Browse files
mpstatonclaude
andcommitted
fix(corpora-curator): the domain type stops filtering the list, so a slow workspace load can no longer hide every corpus (gh #88)
An operator on humain-vc opened the Corpora Curator and was told they had no corpora. They have nine theses. The surface had guessed `strategy` for the domain type, queried `domain.list` with it, got nothing back, and rendered "No corpora yet" — a sentence indistinguishable from the truth. Same symptom as the NATS-subject failure earlier this week, entirely different cause, and this one is ours. The tell was visual rather than logged: the workspace rendered as a pill instead of a `<select>` while the connection read `open`, which has exactly one meaning — `workspace.list` never arrived while the socket stayed healthy. `defaultDomainTypeFor` then fell through to its `'strategy'` default and poisoned every call downstream. We fixed this symptom once already, on 2026-07-07. That fix repaired the SWITCHING path and left the guess in place, so the bug sat dormant for a month waiting for `workspace.list` to be slow once. Guessing better is not a fix. The fix is to stop needing the guess. The list is no longer filtered. `domain.list` is called with `client_slug` only. `listDomains` in the resolver has always declared `type` as optional and projected it in the result, so omitting it is the supported shape — the client was imposing a narrowing the backend never asked for. The types carry no behavioural difference to narrow on; they are vocabulary preference, reach-edu saying "strategy" where humain-vc says "thesis", and domains were abstracted precisely so that any type shows up. The type now travels with the corpus. A new `callType` getter reads `active.type` — a property of the thing the operator clicked — and feeds `domain.assemble`, every `source.*` and `tag.apply`, about fifteen call sites. `domainType` demotes to the create form's default and nothing else, so a wrong value can mis-prefill one visible text field the operator can see and change rather than emptying the surface. The broadcast handler drops its type check for the same reason, which also fixes a retype OUT of the active type being missed. Selection keys on `(type, slug)`, because that is the real key — "apprenticeship" can be a strategy AND a topic. `ACTIVE_STRATEGY_KEY` stores `type:slug`; bare slugs from before this change still resolve, so nobody loses their selection on upgrade. Each row grows a type chip so two same-slug corpora stay tellable apart. No backend change. Three regression tests pin it, including the one that matters: a workspace whose summary never arrives at all still sees every corpus it owns. Files changed: - apps/corpora-curator/src/curation.svelte.ts — callType, activeType, the unfiltered domain.list, the type:slug storage key - apps/corpora-curator/src/App.svelte — broadcast handler loses its type check; the header pill reads the selected corpus's own type - apps/corpora-curator/src/CorpusPicker.svelte — the Type field tracks the workspace preference until the operator overrides it; per-row type chip - apps/corpora-curator/src/app.css — .cc-strat-meta, the row that carries it - apps/corpora-curator/test/curation.test.ts — three regressions - context-v/issues/Domain-Type-Is-Ambient-State-So-A-Failed-Workspace-Load-Hides-Every-Corpus.md - changelog/2026-08-08_05_Same-Symptom-Second-Cause-The-Type-Filter-That-Hid-A-Whole-Workspace.md Closes #88. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PZJvZWco3z7SR2dEhFqEjA
1 parent 7e435d5 commit 01e0f3a

7 files changed

Lines changed: 460 additions & 57 deletions

apps/corpora-curator/src/App.svelte

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@
3838
ev.subject === 'domain.retyped'
3939
? (payload.client_slugs ?? []).includes(curation.clientSlug ?? '')
4040
: payload.client_slug === curation.clientSlug;
41-
const touchesActiveType = payload.type === curation.domainType || payload.old_type === curation.domainType;
42-
if (inThisClient && touchesActiveType) void curation.loadStrategies();
41+
// No type check any more: the list holds every type in the workspace
42+
// (gh #88), so any domain created or retyped in this client is relevant.
43+
// The old filter also meant a retype OUT of the active type was missed
44+
// whenever the guessed type was wrong.
45+
if (inThisClient) void curation.loadStrategies();
4346
} else if (
4447
ev.subject === 'source.added' ||
4548
ev.subject === 'source.updated' ||
@@ -70,11 +73,16 @@
7073
{:else}
7174
<span class="cc-pill" title="Active workspace">{curation.clientSlug ?? '— no workspace —'}</span>
7275
{/if}
73-
<span class="cc-pill" title="Active domain type">{curation.domainType}</span>
76+
<!-- The selected corpus's OWN type when there is one; otherwise this
77+
client's preferred vocabulary. It labels, it never filters (gh #88). -->
78+
<span
79+
class="cc-pill"
80+
title={curation.active ? 'Type of the selected corpus' : 'This workspace’s preferred vocabulary for new corpora'}
81+
>{curation.active?.type ?? curation.domainType}</span>
7482
{#if curation.active}
7583
<button
7684
class="cc-back"
77-
onclick={() => (curation.activeSlug = null)}
85+
onclick={() => { curation.activeSlug = null; curation.activeType = null; }}
7886
title="Back to the corpora list / create form"
7987
>‹ All corpora</button>
8088
<span class="cc-strategy">{curation.active.title}</span>

apps/corpora-curator/src/CorpusPicker.svelte

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@
1818
let title = $state('');
1919
let slug = $state('');
2020
let slugEdited = $state(false);
21+
// A one-time snapshot here was the last visible trace of gh #88: this
22+
// component mounts before workspace.list resolves, so humain-vc's create form
23+
// offered "strategy" while every other surface had corrected itself to
24+
// "thesis". Track the workspace's preference until the operator overrides it;
25+
// once they type, their value wins and stops moving under them.
2126
let type = $state(curation.domainType);
27+
let typeEdited = $state(false);
28+
$effect(() => {
29+
const preferred = curation.domainType;
30+
if (!typeEdited) type = preferred;
31+
});
2232
let tagInput = $state('');
2333
let pendingTags = $state<string[]>([]);
2434
let tagSuggest = $derived(curation.suggestTags(tagInput));
@@ -43,12 +53,12 @@
4353
}
4454
async function create(): Promise<void> {
4555
await curation.createStrategy({ title, slug, tags: pendingTags, type });
46-
// Read domainType back AFTER the await — createStrategy only updates it
47-
// (via setDomainType) once the server confirms a type change, so
48-
// resetting eagerly here would just re-read the pre-create value.
56+
// The form goes back to the workspace's preference after a create — a type
57+
// the operator typed for ONE corpus should not become sticky for the next.
4958
title = '';
5059
slug = '';
5160
slugEdited = false;
61+
typeEdited = false;
5262
type = curation.domainType;
5363
pendingTags = [];
5464
tagInput = '';
@@ -65,9 +75,15 @@
6575
<ul class="cc-strat-list">
6676
{#each curation.strategies as s (s.slug)}
6777
<li>
68-
<button class="cc-strat" onclick={() => curation.select(s.slug)}>
78+
<!-- The type rides along on the row rather than filtering the list
79+
(gh #88). Two corpora can share a slug across types, so the chip
80+
is also what makes them distinguishable. -->
81+
<button class="cc-strat" onclick={() => curation.select(s.slug, s.type)}>
6982
<span class="cc-strat-title">{s.title}</span>
70-
<span class="cc-muted cc-mono cc-mini">{s.slug}</span>
83+
<span class="cc-strat-meta">
84+
<span class="cc-muted cc-mono cc-mini">{s.slug}</span>
85+
{#if s.type}<span class="cc-status-chip">{s.type}</span>{/if}
86+
</span>
7187
</button>
7288
</li>
7389
{/each}
@@ -85,7 +101,12 @@
85101

86102
<div class="cc-field">
87103
<span class="cc-label">Type <span class="cc-muted cc-mini">— any value; 'strategy' and 'thesis' are the two in use today</span></span>
88-
<input class="cc-mono" bind:value={type} placeholder="strategy" />
104+
<input
105+
class="cc-mono"
106+
value={type}
107+
oninput={(e) => { typeEdited = true; type = e.currentTarget.value; }}
108+
placeholder={curation.domainType}
109+
/>
89110
</div>
90111

91112
<div class="cc-field">

apps/corpora-curator/src/app.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@
167167
.cc-app .cc-strat-list { list-style: none; margin: 0.5rem 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
168168
.cc-app .cc-strat { width: 100%; text-align: left; display: flex; flex-direction: column; gap: 2px; }
169169
.cc-app .cc-strat-title { font-weight: 600; }
170+
/* Slug + type on one line. The type chip is load-bearing: the list holds every
171+
type at once (gh #88) and (type, slug) is the real key. */
172+
.cc-app .cc-strat-meta { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
170173
.cc-app .cc-create { display: flex; gap: 0.4rem; margin-top: 0.75rem; }
171174

172175
/* ---- source list ---- */

0 commit comments

Comments
 (0)