Governed Generation — Nav Rename and Dropdown Consolidation Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Rename "Constrained Generation" → "Governed Generation" in the tool nav and consolidate the five top-level accordions inside the Governed Generation tool into three (Phonemes, Psycholinguistics, Contrastive Sets), each with a mode toggle (BAN|BOOST or MINPAIR|MAXOPP).
Architecture: Frontend-only change. The store schema, constraint compiler, API contract, and backend are all unchanged. Two current accordions (ExclusionSection + InclusionSection) that already live inside PhonemeConstraints.tsx merge into a single toggle-driven accordion. Two separate section files (BoundsSection.tsx + WordListBoostSection.tsx) merge into a new PsycholinguisticsSection.tsx. ContrastiveSection.tsx only changes toggle button labels. Nav rename is isolated to App_new.tsx.
Tech Stack: React 19, TypeScript, MUI 7, Zustand. Existing tests: Vitest with React Testing Library (store + compiler tests only — no component tests). Verification is primarily manual browser check + type-check + build, following the existing pattern for UI work in this codebase.
Spec: docs/superpowers/specs/2026-04-18-governed-gen-nav-layout-design.md
Branch: feature/phon-41-governed-gen-nav-layout (off develop)
Ticket: PHON-41 (parent PHON-4)
File inventory¶
Modified:
- packages/web/frontend/src/App_new.tsx
- packages/web/frontend/src/components/tools/GovernedGenerationTool/index.tsx
- packages/web/frontend/src/components/tools/GovernedGenerationTool/PhonemeConstraints.tsx
- packages/web/frontend/src/components/tools/GovernedGenerationTool/ContrastiveSection.tsx
Created:
- packages/web/frontend/src/components/tools/GovernedGenerationTool/PsycholinguisticsSection.tsx
Deleted:
- packages/web/frontend/src/components/tools/GovernedGenerationTool/BoundsSection.tsx
- packages/web/frontend/src/components/tools/GovernedGenerationTool/WordListBoostSection.tsx
Task 1: Nav rename in App_new.tsx¶
Files:
- Modify: packages/web/frontend/src/App_new.tsx (lines 117–124, 133)
- [ ] Step 1.1: Change id, title, and description in TOOL_DEFINITIONS entry
Replace this block (currently lines 117–124):
{
id: 'constrainedGeneration',
icon: <GovernedGenIcon />,
title: 'Constrained Generation',
description: 'Constrained content generation with phonological targets, psycholinguistic bounds, and compliance analysis.',
color: TOOL_COLORS.governedGeneration,
section: 'explore',
},
With:
{
id: 'governedGeneration',
icon: <GovernedGenIcon />,
title: 'Governed Generation',
description: 'Governed content generation with phonological targets, psycholinguistic bounds, and compliance analysis.',
color: TOOL_COLORS.governedGeneration,
section: 'explore',
},
- [ ] Step 1.2: Update key in TOOL_COMPONENTS map
Find the line:
constrainedGeneration: () => <GovernedGenerationTool />,
Replace with:
governedGeneration: () => <GovernedGenerationTool />,
- [ ] Step 1.3: Type-check
Run: cd packages/web/frontend && npm run type-check
Expected: passes with no errors.
- [ ] Step 1.4: Manual browser check
Dev server should already be running on http://localhost:3000. If not, start with cd packages/web/frontend && npm run dev.
In the tool nav, confirm the tool label reads "Governed Generation" (not "Constrained Generation"). Clicking it should open the same tool as before.
- [ ] Step 1.5: Commit
git add packages/web/frontend/src/App_new.tsx
git commit -m "feat(web): rename Constrained Generation → Governed Generation in tool nav
PHON-41. Update TOOL_DEFINITIONS id/title/description and matching
TOOL_COMPONENTS key. No URL routing or deep-link consumers."
Task 2: Rename Contrastive toggle labels¶
Files:
- Modify: packages/web/frontend/src/components/tools/GovernedGenerationTool/ContrastiveSection.tsx (lines 87–88)
- [ ] Step 2.1: Shorten ToggleButton labels
Find:
<ToggleButton value="minpair">Minimal Pair</ToggleButton>
<ToggleButton value="maxopp">Maximal Opposition</ToggleButton>
Replace with:
<ToggleButton value="minpair">MINPAIR</ToggleButton>
<ToggleButton value="maxopp">MAXOPP</ToggleButton>
- [ ] Step 2.2: Type-check
Run: cd packages/web/frontend && npm run type-check
Expected: passes.
- [ ] Step 2.3: Manual browser check
Open the Governed Generation tool, expand "Contrastive Pairs", confirm toggle reads MINPAIR | MAXOPP. Existing chips render as before (chip labels already abbreviate as MinPair / MaxOpp).
- [ ] Step 2.4: Commit
git add packages/web/frontend/src/components/tools/GovernedGenerationTool/ContrastiveSection.tsx
git commit -m "feat(web): shorten Contrastive toggle labels to MINPAIR | MAXOPP
PHON-41. Parallel short-caps style shared with the new BAN | BOOST
toggles on Phonemes and Psycholinguistics accordions."
Task 3: Rewrite PhonemeConstraints.tsx as a single toggle-driven accordion¶
Files:
- Modify (full rewrite): packages/web/frontend/src/components/tools/GovernedGenerationTool/PhonemeConstraints.tsx
- [ ] Step 3.1: Replace file contents
Overwrite the file with:
/**
* Phonemes accordion — single dropdown with BAN | BOOST mode toggle.
*
* Toggle at top selects which mode is active for the "add next" form.
* Both BAN (exclude) and BOOST (include) chip lists render below and
* remain editable regardless of toggle state. Uses PhonemePickerDialog
* for visual phoneme selection.
*/
import { useState } from 'react';
import type { KeyboardEvent } from 'react';
import {
Accordion,
AccordionSummary,
AccordionDetails,
Typography,
TextField,
IconButton,
Box,
Chip,
Stack,
Slider,
InputAdornment,
ToggleButton,
ToggleButtonGroup,
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
Keyboard as KeyboardIcon,
} from '@mui/icons-material';
import PhonemePickerDialog from '../../PhonemePickerDialog';
import { useConstraintStore } from '../../../store/constraintStore';
import type { StoreEntry } from '../../../types/governance';
type Mode = 'ban' | 'boost';
export default function PhonemeConstraints() {
const { entries, add, remove } = useConstraintStore();
const [mode, setMode] = useState<Mode>('ban');
const [pickerOpen, setPickerOpen] = useState(false);
const [input, setInput] = useState('');
const excludeEntries = entries.filter(
(e): e is Extract<StoreEntry, { type: 'exclude' }> => e.type === 'exclude',
);
const includeEntries = entries.filter(
(e): e is Extract<StoreEntry, { type: 'include' }> => e.type === 'include',
);
const addPhoneme = (phoneme: string) => {
const trimmed = phoneme.trim();
if (!trimmed) return;
if (mode === 'ban') {
add({ type: 'exclude', phoneme: trimmed });
} else {
add({ type: 'include', phoneme: trimmed, strength: 2.0, targetRate: 20 });
}
};
const handleInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && input.trim()) {
input.trim().split(/\s+/).forEach(addPhoneme);
setInput('');
}
};
const updateCoverage = (phoneme: string, targetRate: number) => {
const existing = includeEntries.find((e) => e.phoneme === phoneme);
if (!existing) return;
remove('include', { phoneme });
add({
type: 'include',
phoneme,
strength: existing.strength,
targetRate: targetRate > 0 ? targetRate : undefined,
});
};
const banCount = excludeEntries.length;
const boostCount = includeEntries.length;
const helperCopy =
mode === 'ban'
? 'Block phonemes from appearing in generated output.'
: 'Encourage phonemes in generated output. Set a coverage target — e.g., 20% means roughly 1 in 5 content words will contain the phoneme. The system self-regulates across the output.';
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1">
Phonemes
{(banCount > 0 || boostCount > 0) && (
<Box component="span" sx={{ ml: 1, display: 'inline-flex', gap: 0.5 }}>
{banCount > 0 && (
<Chip label={`${banCount} BAN`} size="small" color="error" />
)}
{boostCount > 0 && (
<Chip label={`${boostCount} BOOST`} size="small" color="success" />
)}
</Box>
)}
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ px: { xs: 1.5, sm: 2 }, py: { xs: 1, sm: 2 } }}>
<Stack spacing={2}>
<ToggleButtonGroup
value={mode}
exclusive
onChange={(_, v: Mode | null) => { if (v) setMode(v); }}
size="small"
>
<ToggleButton value="ban">BAN</ToggleButton>
<ToggleButton value="boost">BOOST</ToggleButton>
</ToggleButtonGroup>
<Typography variant="caption" color="text.secondary" display="block">
{helperCopy}
</Typography>
<TextField
fullWidth
size="small"
placeholder="Type phonemes or use picker..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleInputKeyDown}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setPickerOpen(true)} edge="end" color="primary" size="small">
<KeyboardIcon />
</IconButton>
</InputAdornment>
),
},
}}
/>
{excludeEntries.length > 0 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{excludeEntries.map((entry) => (
<Chip
key={entry.phoneme}
label={`/${entry.phoneme}/`}
color="error"
size="small"
variant="outlined"
onDelete={() => remove('exclude', { phoneme: entry.phoneme })}
/>
))}
</Box>
)}
{includeEntries.length > 0 && (
<Stack spacing={2}>
{includeEntries.map((entry) => (
<Stack key={entry.phoneme} direction="row" spacing={2} alignItems="center">
<Chip
label={`/${entry.phoneme}/`}
color="success"
size="small"
variant="outlined"
onDelete={() => remove('include', { phoneme: entry.phoneme })}
/>
<Box sx={{ flex: 1, maxWidth: 200 }}>
<Typography variant="caption" color="text.secondary">
Coverage: {entry.targetRate ?? 20}%
</Typography>
<Slider
size="small"
value={entry.targetRate ?? 20}
onChange={(_, v) => updateCoverage(entry.phoneme, v as number)}
min={5}
max={50}
step={5}
valueLabelDisplay="auto"
valueLabelFormat={(v) => `${v}%`}
/>
</Box>
</Stack>
))}
</Stack>
)}
</Stack>
<PhonemePickerDialog
open={pickerOpen}
onClose={() => setPickerOpen(false)}
onSelect={addPhoneme}
/>
</AccordionDetails>
</Accordion>
);
}
- [ ] Step 3.2: Type-check
Run: cd packages/web/frontend && npm run type-check
Expected: passes.
- [ ] Step 3.3: Run existing tests
Run: cd packages/web/frontend && npm test -- --run
Expected: all tests pass (store + compiler tests are untouched and should remain green).
- [ ] Step 3.4: Manual browser check
In the Governed Generation tool:
1. Only ONE "Phonemes" accordion appears (not two — no more "Phoneme Exclusion" / "Phoneme Inclusion" headers).
2. Inside, a BAN | BOOST toggle sits at the top.
3. With mode = BAN, typing a phoneme and pressing Enter adds a red chip to the BAN list.
4. Flipping to BOOST, typing a phoneme and pressing Enter adds a green chip with a coverage slider to the BOOST list.
5. Flipping back to BAN does NOT hide the BOOST chips — both lists remain visible and deletable.
6. Summary chip in the accordion header (when collapsed) shows e.g. 1 BAN 2 BOOST side by side.
- [ ] Step 3.5: Commit
git add packages/web/frontend/src/components/tools/GovernedGenerationTool/PhonemeConstraints.tsx
git commit -m "feat(web): merge Phoneme Exclusion + Inclusion into single Phonemes accordion
PHON-41. One accordion, BAN | BOOST toggle selects add-mode, both chip
lists stay visible. Accordion-header summary shows ban/boost counts
side-by-side. Store schema unchanged."
Task 4: Create PsycholinguisticsSection.tsx¶
Files:
- Create: packages/web/frontend/src/components/tools/GovernedGenerationTool/PsycholinguisticsSection.tsx
- [ ] Step 4.1: Write the new file
Create the file with:
/**
* Psycholinguistics accordion — single dropdown with BAN | BOOST mode toggle.
*
* Merges the former BoundsSection (hard exclude by norm threshold) and
* WordListBoostSection (soft boost by norm threshold + coverage target)
* into one accordion. Property lists differ by mode:
* - BAN uses BOUNDS (includes frequency_percentile)
* - BOOST uses BOOST_NORMS (no frequency_percentile)
* Both chip lists render below the add form, always visible.
*/
import { useState } from 'react';
import {
Accordion,
AccordionSummary,
AccordionDetails,
Typography,
Stack,
Chip,
Slider,
Box,
Select,
MenuItem,
FormControl,
InputLabel,
Button,
ToggleButton,
ToggleButtonGroup,
} from '@mui/material';
import { ExpandMore as ExpandMoreIcon, Add as AddIcon } from '@mui/icons-material';
import { useConstraintStore } from '../../../store/constraintStore';
import type { StoreEntry } from '../../../types/governance';
type Mode = 'ban' | 'boost';
interface NormDef {
norm: string;
label: string;
description: string;
min: number;
max: number;
step: number;
direction: 'max' | 'min';
format: (v: number) => string;
}
const BOUNDS: NormDef[] = [
{
norm: 'aoa_kuperman', label: 'Age of Acquisition',
description: 'Exclude words acquired after this age (Kuperman, years)',
min: 2, max: 21, step: 0.5, direction: 'max',
format: (v) => `≤ ${v} yrs`,
},
{
norm: 'concreteness', label: 'Concreteness',
description: 'Exclude words below this concreteness (1 = abstract, 5 = concrete)',
min: 1, max: 5, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
{
norm: 'familiarity', label: 'Familiarity',
description: 'Exclude words below this familiarity (1 = rare, 7 = very familiar)',
min: 1, max: 7, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
{
norm: 'imageability', label: 'Imageability',
description: 'Exclude words below this imageability (1 = hard to picture, 7 = easy)',
min: 1, max: 7, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
{
norm: 'frequency_percentile', label: 'Word Frequency',
description: 'Exclude words below this frequency percentile',
min: 0, max: 100, step: 5, direction: 'min',
format: (v) => `≥ ${v}th %ile`,
},
{
norm: 'valence', label: 'Valence',
description: 'Exclude words below this valence (1 = negative, 9 = positive)',
min: 1, max: 9, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
{
norm: 'arousal', label: 'Arousal',
description: 'Exclude words above this arousal (1 = calm, 9 = excited)',
min: 1, max: 9, step: 0.5, direction: 'max',
format: (v) => `≤ ${v}`,
},
{
norm: 'dominance', label: 'Dominance',
description: 'Exclude words below this dominance (1 = submissive, 9 = dominant)',
min: 1, max: 9, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
];
const BOOST_NORMS: NormDef[] = [
{
norm: 'aoa_kuperman', label: 'Age of Acquisition',
description: 'Boost words acquired by this age',
min: 2, max: 21, step: 0.5, direction: 'max',
format: (v) => `≤ ${v} yrs`,
},
{
norm: 'concreteness', label: 'Concreteness',
description: 'Boost concrete/tangible words',
min: 1, max: 5, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
{
norm: 'familiarity', label: 'Familiarity',
description: 'Boost familiar/well-known words',
min: 1, max: 7, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
{
norm: 'imageability', label: 'Imageability',
description: 'Boost easy-to-visualize words',
min: 1, max: 7, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
{
norm: 'valence', label: 'Valence',
description: 'Boost positive-valence words',
min: 1, max: 9, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
{
norm: 'arousal', label: 'Arousal',
description: 'Boost high-arousal words',
min: 1, max: 9, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
{
norm: 'dominance', label: 'Dominance',
description: 'Boost high-dominance words',
min: 1, max: 9, step: 0.5, direction: 'min',
format: (v) => `≥ ${v}`,
},
];
export default function PsycholinguisticsSection() {
const { entries, add, remove } = useConstraintStore();
const [mode, setMode] = useState<Mode>('ban');
const [selectedNorm, setSelectedNorm] = useState('');
const [value, setValue] = useState<number | null>(null);
const [coverage, setCoverage] = useState(20);
const boundEntries = entries.filter(
(e): e is Extract<StoreEntry, { type: 'bound' }> => e.type === 'bound',
);
const boostEntries = entries.filter(
(e): e is Extract<StoreEntry, { type: 'bound_boost' }> => e.type === 'bound_boost',
);
const defs = mode === 'ban' ? BOUNDS : BOOST_NORMS;
const activeDef = defs.find((d) => d.norm === selectedNorm);
const handleSelectChange = (normKey: string) => {
setSelectedNorm(normKey);
const def = defs.find((d) => d.norm === normKey);
if (def) {
if (mode === 'ban') {
setValue(def.direction === 'max' ? Math.round(def.max * 0.6) : Math.round(def.min + (def.max - def.min) * 0.4));
} else {
setValue(def.direction === 'max' ? def.max : def.min);
}
}
};
const handleAdd = () => {
if (!activeDef || value === null) return;
if (mode === 'ban') {
add({ type: 'bound', norm: activeDef.norm, direction: activeDef.direction, value });
} else {
add({
type: 'bound_boost',
norm: activeDef.norm,
direction: activeDef.direction,
value,
coverageTarget: coverage,
});
}
setSelectedNorm('');
setValue(null);
};
const banCount = boundEntries.length;
const boostCount = boostEntries.length;
const helperCopy =
mode === 'ban'
? 'Exclude words outside property thresholds. Function words (the, a, is, etc.) are always allowed.'
: 'Encourage words that meet psycholinguistic criteria. Set a coverage target for how many content words should come from the boosted list.';
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1">
Psycholinguistics
{(banCount > 0 || boostCount > 0) && (
<Box component="span" sx={{ ml: 1, display: 'inline-flex', gap: 0.5 }}>
{banCount > 0 && (
<Chip label={`${banCount} BAN`} size="small" color="error" />
)}
{boostCount > 0 && (
<Chip label={`${boostCount} BOOST`} size="small" color="success" />
)}
</Box>
)}
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ px: { xs: 1.5, sm: 2 }, py: { xs: 1, sm: 2 } }}>
<Stack spacing={2}>
<ToggleButtonGroup
value={mode}
exclusive
onChange={(_, v: Mode | null) => {
if (!v) return;
setMode(v);
setSelectedNorm('');
setValue(null);
}}
size="small"
>
<ToggleButton value="ban">BAN</ToggleButton>
<ToggleButton value="boost">BOOST</ToggleButton>
</ToggleButtonGroup>
<Typography variant="caption" color="text.secondary" display="block">
{helperCopy}
</Typography>
<FormControl size="small" fullWidth>
<InputLabel>Property</InputLabel>
<Select
value={selectedNorm}
label="Property"
onChange={(e) => handleSelectChange(e.target.value)}
>
{defs.map((d) => (
<MenuItem key={d.norm} value={d.norm}>{d.label}</MenuItem>
))}
</Select>
</FormControl>
{activeDef && value !== null && (
<>
<Box>
<Typography variant="caption" color="text.secondary">
{activeDef.description}: {activeDef.format(value)}
</Typography>
<Slider
size="small"
value={value}
onChange={(_, v) => setValue(v as number)}
min={activeDef.min}
max={activeDef.max}
step={activeDef.step}
valueLabelDisplay="auto"
valueLabelFormat={activeDef.format}
track={mode === 'ban' && activeDef.direction === 'max' ? 'inverted' : 'normal'}
/>
</Box>
{mode === 'boost' && (
<Box>
<Typography variant="caption" color="text.secondary">
Coverage target: ~{coverage}% of content words
</Typography>
<Slider
size="small"
value={coverage}
onChange={(_, v) => setCoverage(v as number)}
min={5}
max={50}
step={5}
valueLabelDisplay="auto"
valueLabelFormat={(v) => `${v}%`}
/>
</Box>
)}
<Button
size="small"
variant="outlined"
startIcon={<AddIcon />}
onClick={handleAdd}
>
{mode === 'ban' ? 'Add bound' : 'Add boost'}
</Button>
</>
)}
{boundEntries.length > 0 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{boundEntries.map((e, i) => {
const def = BOUNDS.find((b) => b.norm === e.norm);
const label = def
? `${def.label} ${def.format(e.value)}`
: `${e.norm} ${e.direction === 'min' ? '≥' : '≤'} ${e.value}`;
return (
<Chip
key={`ban-${i}`}
label={label}
size="small"
color="error"
variant="outlined"
onDelete={() => remove('bound', { norm: e.norm, direction: e.direction })}
/>
);
})}
</Box>
)}
{boostEntries.length > 0 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{boostEntries.map((e, i) => {
const def = BOOST_NORMS.find((n) => n.norm === e.norm);
const label = def
? `${def.label} ${def.format(e.value)} ~${e.coverageTarget}%`
: `${e.norm} ${e.value}`;
return (
<Chip
key={`boost-${i}`}
label={label}
size="small"
color="success"
variant="outlined"
onDelete={() => remove('bound_boost', { norm: e.norm, direction: e.direction })}
/>
);
})}
</Box>
)}
</Stack>
</AccordionDetails>
</Accordion>
);
}
- [ ] Step 4.2: Type-check
Run: cd packages/web/frontend && npm run type-check
Expected: passes. (The file is not yet wired into index.tsx; tree-shaking or unused warnings will not block type-check.)
- [ ] Step 4.3: Commit (intermediate)
git add packages/web/frontend/src/components/tools/GovernedGenerationTool/PsycholinguisticsSection.tsx
git commit -m "feat(web): add merged PsycholinguisticsSection component
PHON-41. Single accordion with BAN | BOOST toggle replacing the
separate BoundsSection and WordListBoostSection components. Not yet
wired into index.tsx — that happens in Task 5 along with deletion of
the old files."
Task 5: Wire PsycholinguisticsSection into index.tsx, delete old files¶
Files:
- Modify: packages/web/frontend/src/components/tools/GovernedGenerationTool/index.tsx
- Delete: packages/web/frontend/src/components/tools/GovernedGenerationTool/BoundsSection.tsx
- Delete: packages/web/frontend/src/components/tools/GovernedGenerationTool/WordListBoostSection.tsx
- [ ] Step 5.1: Update index.tsx imports
In packages/web/frontend/src/components/tools/GovernedGenerationTool/index.tsx, find this import block (currently lines 25–30):
import PhonemeConstraints from './PhonemeConstraints';
import BoundsSection from './BoundsSection';
import WordListBoostSection from './WordListBoostSection';
import ContrastiveSection from './ContrastiveSection';
import ActiveConstraints from './ActiveConstraints';
import OutputFeed from './OutputFeed';
Replace with:
import PhonemeConstraints from './PhonemeConstraints';
import PsycholinguisticsSection from './PsycholinguisticsSection';
import ContrastiveSection from './ContrastiveSection';
import ActiveConstraints from './ActiveConstraints';
import OutputFeed from './OutputFeed';
- [ ] Step 5.2: Update index.tsx JSX
In the same file, find the constraint composition block (currently lines 153–157):
{/* Constraint composition */}
<PhonemeConstraints />
<BoundsSection />
<WordListBoostSection />
<ContrastiveSection />
Replace with:
{/* Constraint composition */}
<PhonemeConstraints />
<PsycholinguisticsSection />
<ContrastiveSection />
- [ ] Step 5.3: Delete the two retired files
git rm packages/web/frontend/src/components/tools/GovernedGenerationTool/BoundsSection.tsx packages/web/frontend/src/components/tools/GovernedGenerationTool/WordListBoostSection.tsx
Expected: both files removed from the working tree and staged for deletion.
- [ ] Step 5.4: Type-check
Run: cd packages/web/frontend && npm run type-check
Expected: passes. No imports should reference the deleted files.
- [ ] Step 5.5: Lint check
Run: cd packages/web/frontend && npm run lint
Expected: passes (max-warnings 50). Fix any issues inline.
- [ ] Step 5.6: Run existing tests
Run: cd packages/web/frontend && npm test -- --run
Expected: all pass.
- [ ] Step 5.7: Full production build
Run: cd packages/web/frontend && npm run build
Expected: builds cleanly.
- [ ] Step 5.8: End-to-end manual browser check
Open http://localhost:3000 (dev server), navigate to the Governed Generation tool:
- Only THREE accordions now:
Phonemes,Psycholinguistics,Contrastive Sets(in that order). - Each has its own toggle —
BAN | BOOSTon the first two,MINPAIR | MAXOPPon the third. - Add a ban and a boost in Phonemes — both chips appear; accordion header (when collapsed) shows
1 BAN 1 BOOST. - Add a bound and a bound-boost in Psycholinguistics — both chips appear with correct labels; BAN chip renders red, BOOST chip renders green (red is a change from the previous blue).
- Contrastive works as before — toggle shows
MINPAIR | MAXOPP, chips render with correct labels. ActiveConstraintschip summary below the three accordions still renders all chips correctly with delete behavior intact.-
Compose a full set of constraints and click Generate (requires generation server running; if not, skip — the generate button being disabled is expected).
-
[ ] Step 5.9: Commit
git add packages/web/frontend/src/components/tools/GovernedGenerationTool/index.tsx
git commit -m "feat(web): wire PsycholinguisticsSection, retire separate Bounds/Boost files
PHON-41. Governed Generation now presents three top-level accordions
(Phonemes, Psycholinguistics, Contrastive Sets) instead of five. Old
BoundsSection and WordListBoostSection components deleted; their
behavior is preserved inside PsycholinguisticsSection with a
BAN | BOOST toggle.
Closes PHON-41."
Task 6: Final branch verification¶
- [ ] Step 6.1: Confirm working tree is clean
Run: git status
Expected: nothing to commit, working tree clean
- [ ] Step 6.2: Confirm commit chain
Run: git log --oneline origin/develop..HEAD
Expected: seven commits on top of origin/develop:
docs: design spec for PHON-41 governed gen nav + dropdown consolidation(committed at end of brainstorming)docs: implementation plan for PHON-41 governed gen nav + dropdown consolidation(committed at end of plan writing)feat(web): rename Constrained Generation → Governed Generation in tool navfeat(web): shorten Contrastive toggle labels to MINPAIR | MAXOPPfeat(web): merge Phoneme Exclusion + Inclusion into single Phonemes accordionfeat(web): add merged PsycholinguisticsSection component-
feat(web): wire PsycholinguisticsSection, retire separate Bounds/Boost files -
[ ] Step 6.3: Confirm deleted files are gone
Run: ls packages/web/frontend/src/components/tools/GovernedGenerationTool/
Expected: no BoundsSection.tsx and no WordListBoostSection.tsx in the listing. Should see ContrastiveSection.tsx, PhonemeConstraints.tsx, PsycholinguisticsSection.tsx, plus the pre-existing ActiveConstraints.tsx, OutputCard.tsx, OutputFeed.tsx, TokenDisplay.tsx, index.tsx.
- [ ] Step 6.4: Ready for review
Stop here. Do not push to origin or open a PR without explicit user approval — memory feedback_no_premature_merge applies.
Post-merge follow-ups (separate tickets / manual updates)¶
- Update PHON-41 ticket description wording from "5 top-level dropdowns" to reflect the 3-accordion outcome.
- Clean up stale comment on
App_new.tsxline 16 (// - Governed Generation (coming soon)) — unrelated to PHON-41 scope but trivial. - Update
reference_jira_backlog.mdto mark PHON-41 Done.