Skip to content

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:

  1. Only THREE accordions now: Phonemes, Psycholinguistics, Contrastive Sets (in that order).
  2. Each has its own toggle — BAN | BOOST on the first two, MINPAIR | MAXOPP on the third.
  3. Add a ban and a boost in Phonemes — both chips appear; accordion header (when collapsed) shows 1 BAN 1 BOOST.
  4. 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).
  5. Contrastive works as before — toggle shows MINPAIR | MAXOPP, chips render with correct labels.
  6. ActiveConstraints chip summary below the three accordions still renders all chips correctly with delete behavior intact.
  7. Compose a full set of constraints and click Generate (requires generation server running; if not, skip — the generate button being disabled is expected).

  8. [ ] 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:

  1. docs: design spec for PHON-41 governed gen nav + dropdown consolidation (committed at end of brainstorming)
  2. docs: implementation plan for PHON-41 governed gen nav + dropdown consolidation (committed at end of plan writing)
  3. feat(web): rename Constrained Generation → Governed Generation in tool nav
  4. feat(web): shorten Contrastive toggle labels to MINPAIR | MAXOPP
  5. feat(web): merge Phoneme Exclusion + Inclusion into single Phonemes accordion
  6. feat(web): add merged PsycholinguisticsSection component
  7. feat(web): wire PsycholinguisticsSection, retire separate Bounds/Boost files

  8. [ ] 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.tsx line 16 (// - Governed Generation (coming soon)) — unrelated to PHON-41 scope but trivial.
  • Update reference_jira_backlog.md to mark PHON-41 Done.