fix(m3): AI-assist scope + confidence gate + grouped wood categories

- inference: _taxonomy_lists hands the model the synced species leaves + wood
  leaves + case-insensitive attribute terms (was furniture roots / empty)
- inference: CALL1_SEED pins exact flat schema + *_confidence keys, drops grade
- inference: _gate maps field->confidence key correctly (was species_id_confidence),
  reads nested shape as safety net, omits grade (METADATA-40)
- review.svelte: figure gets AI assist, grade dropped from AI (owner judgment)
- MultiSelect: grouped mode (parents non-selectable headers, selectable leaves)
- METADATA-40 updated: grade not suggested by Call 1
This commit is contained in:
Ty
2026-09-20 21:47:36 -07:00
parent e36979617b
commit cec03c5bd3
5 changed files with 212 additions and 82 deletions
+28 -15
View File
@@ -30,7 +30,7 @@
// --- Derived taxonomy option lists (resolve by slug, never hardcoded ids) ---
let speciesOpts: Opt[] = [];
let woodCatOpts: Opt[] = [];
let woodCatGroups: import('./MultiSelect.svelte').Group[] = [];
let edgeOpts: Opt[] = [];
let figureOpts: Opt[] = [];
let gradeOpts: Opt[] = [];
@@ -57,7 +57,17 @@
return (attr?.terms ?? []).map((t) => ({ id: t.id, name: t.name, ai: false }));
};
speciesOpts = childrenOf(speciesGrouper).map((c) => ({ id: c.id, name: c.name, ai: false }));
woodCatOpts = childrenOf(wcatGrouper).map((c) => ({ id: c.id, name: c.name, ai: false }));
// Wood categories: parent/child grouped (METADATA-10 — parents group only, never
// assigned). Each direct child of `wcat` that has its own children becomes a parent
// group with those children as leaves; a child with no children is a leaf under the
// (implicit) top group. This keeps the full tree visible with non-selectable parents.
woodCatGroups = (childrenOf(wcatGrouper)).map((parent) => {
const kids = cats.filter((c) => c.parent_id === parent.id);
const leaves: Opt[] = kids.length
? kids.map((c) => ({ id: c.id, name: c.name, ai: false }))
: [{ id: parent.id, name: parent.name, ai: false }];
return { parent: { id: parent.id, name: parent.name }, leaves };
});
edgeOpts = termsOf('edge type');
figureOpts = termsOf('figure');
gradeOpts = termsOf('grade');
@@ -216,7 +226,7 @@
}
if (s.edge_type_term_id) edgeTypeId = s.edge_type_term_id;
if (s.figure_term_ids) { figureIds = [...s.figure_term_ids]; recs.push(...s.figure_term_ids); }
if (s.grade_term_id) gradeId = s.grade_term_id;
// Grade is not AI-suggested (METADATA-40) — owner judgment. Do not auto-fill.
aiRecommendations = recs;
aiNote = recs.length ? `AI suggested ${recs.length} field${recs.length === 1 ? '' : 's'} — review & edit below.` : 'No confident AI suggestions.';
await commitReview();
@@ -330,7 +340,7 @@
<!-- Wood categories (multi, searchable list, AI-assist) -->
<MultiSelect
label="Wood categories"
options={woodCatOpts}
groups={woodCatGroups}
selected={woodCatIds}
placeholder="Choose wood categories"
testid="wood-cat"
@@ -349,18 +359,21 @@
</div>
<!-- Figure (multi, searchable list, AI-assist) -->
<MultiSelect
label="Figure"
options={figureOpts}
selected={figureIds}
placeholder="Choose figure"
testid="figure"
onToggle={(id) => (figureIds = toggleMulti(figureIds, id))}
/>
<!-- Grade / Thickness / Moisture (single, AI-assist) -->
<div class="field">
<label class="lbl" for="grade">Grade <span class="ai">AI assist</span></label>
<label class="lbl">Figure <span class="ai">AI assist</span></label>
<MultiSelect
label="Figure"
options={figureOpts}
selected={figureIds}
placeholder="Choose figure"
testid="figure"
onToggle={(id) => (figureIds = toggleMulti(figureIds, id))}
/>
</div>
<!-- Grade (single, owner judgment — NOT AI-assisted per METADATA-40) -->
<div class="field">
<label class="lbl" for="grade">Grade</label>
<select id="grade" class="sel" data-testid="grade" bind:value={gradeId} on:change={commitReview}>
<option value="">— select —</option>
{#each gradeOpts as o (o.id)}<option value={o.id}>{o.name}</option>{/each}