T-5BAR-pluralize-recognizes-backlog-as-plural
Status: closed/done · Impact: medium · Complexity: small
Auto-generated from a /sdlc:task-work post-mortem. Review and
promote to open/ready before picking up.
pluralize("backlog") in plugin/skills/setup/setup_planning.py returns
"backlogs", but the canonical entity directory is backlog/ and real
planning content lives at docs/planning/backlog/. Every new feature
that loops over entities and derives a destination dir hits this
mismatch and either silently writes to the wrong place or carries a
hard-coded carve-out. The post-mortem of
T-781K-add-obsidian-bases-setup-flag noted this surfaced again
while writing write_bases() and was patched locally — a single
one-line conditional now lives in setup_planning.py. Centralizing the
fix in pluralize() (or renaming the entity) closes the gap once.
plugin/skills/setup/setup_planning.py defines a pluralize(name) helper
that naively appends s (or es for *s/*x/*z). For
name == "backlog" it returns backlogs, which doesn’t match the
on-disk directory name backlog/. The just-shipped write_bases()
function carries a hardcoded carve-out:
“Pre-existing
backlogvsbacklogsdirectory inconsistency surfaced again:pluralize("backlog")producesbacklogsbut real backlog content lives indocs/planning/backlog/. Worked around inwrite_baseswith a hardcoded carve-out.”
The same carve-out will need to be re-applied by any future feature that builds a destination path from the entity name.
Proposed
Section titled “Proposed”A single source of truth in pluralize() (or its replacement) that
returns "backlog" for name == "backlog". All callers — current and
future — derive docs/planning/backlog/ without per-call workarounds.
The hardcoded if entity == "backlog" branch in write_bases() is
removed.
Approach
Section titled “Approach”- Add a small irregular-plural map (or an “already-plural” set) to
pluralize()inplugin/skills/setup/setup_planning.py. Initial entry:"backlog" -> "backlog". Decide whether to keep the helper inline insetup_planning.pyor promote it to a shared module — leave inline if no second caller exists yet. - Delete the carve-out branch in
write_bases()that special-casesentity == "backlog". Verify by re-runningsetup_planning.py --obsidian --dry-runon a fresh tmp dir and confirming the destination is stilldocs/planning/backlog/backlog.base. - Re-run
/sdlc:setupend-to-end on a fresh project to confirm AC-5 from the originating task still holds (no.basefiles when--obsidianis absent; correct destinations when it’s present).
Files to touch
Section titled “Files to touch”plugin/skills/setup/setup_planning.py— extendpluralize()with an irregular-plural carve-out; remove the inlineentity == "backlog"branch fromwrite_bases().plugin/entities/backlog/base.yaml— update thefile.inFolder("planning/backlog")filter to match whichever side of the singular/plural decision wins in AC-1 (the rest of the entity types use plural; deciding to keepbacklog/singular is consistent withpluralize("backlog") == "backlog").plugin/scripts/new_backlog.py— currently writes new backlog files todocs/planning/backlogs/(plural) at line 155; realign to the same singular/plural decision so all callers agree.
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
pluralize("backlog") == "backlog"(assert in a smoke test or quickpython -c). - AC-2:
setup_planning.py --obsidian --dry-runin a fresh tmp project reportsdocs/planning/backlog/backlog.baseas the backlog destination with no special-casing in the calling code. - AC-3:
write_bases()contains noif entity == "backlog"branch. - AC-4: AC-1, AC-3, AC-5 from T-781K-add-obsidian-bases-setup-flag continue to pass after this change.
- AC-5 (absorbed from
T-U6ZX-setup-obsidian-backlog-dir-mismatch): The Bases
file’s
file.inFolder(...)filter references the same directory the file lives in (i.e. nobacklogs/backlog.baseorbacklog/backlogs.basemismatch). - AC-6 (absorbed): After a fresh
/sdlc:setup --obsidianrun on a clean project, only one ofdocs/planning/backlog/ordocs/planning/backlogs/exists — no orphan parallel directory. - AC-7 (absorbed):
new_backlog.pywrites new files to the same directory the entity files live in.
Out of scope
Section titled “Out of scope”- Renaming the
plugin/entities/backlog/directory tobacklogitem/(the heavier alternative noted in the post-mortem). - Generalizing
pluralize()into a full English-plural library — the carve-out map is enough.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:task-work post-mortem of T-781K-add-obsidian-bases-setup-flag on 2026-05-20.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-21. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: agent-manual —
python -cimportedsetup_planning.pluralize; assertedpluralize("backlog") == "backlog"and thattask/bug/epic/feature/milestone/roadmap/storystill pluralize correctly. - AC-2: agent-manual — ran
setup_planning.py --obsidian --dry-runin/tmp/pluralize-ac2-test; output includedwould write: docs/planning/backlog/backlog.basewith no special-casing in the calling code (carve-out branch deleted). - AC-3: auto —
command grepforentity == "backlog"/singular == "backlog"/== 'backlog'insetup_planning.pyreturned no matches. - AC-4: agent-manual — re-ran the originating task’s AC-1/AC-3/AC-5 invariants against a fresh tmp
dir;
.basefiles land indocs/planning/{tasks,epics,milestones,backlog}/, second run without--forceleaves mtime unchanged, run without--obsidianwrites zero.basefiles. - AC-5: auto —
plugin/entities/backlog/base.yamlalready referencesfile.inFolder("planning/backlog"), matching the destinationdocs/planning/backlog/backlog.basewritten by AC-2’s dry-run. - AC-6: agent-manual —
setup_planning.py --obsidianin/tmp/pluralize-ac6-test;ls docs/planning/showed onlybacklog/(no orphanbacklogs/). - AC-7: agent-manual —
new_backlog.py test-slug --project-root .in/tmp/pluralize-ac7-test; new file landed atdocs/planning/backlog/test-slug.md.
What worked
Section titled “What worked”- Two
pluralize()copies (setup_planning.py+audit_entities.py) — the second was easy to find because the audit copy carries a# mirror setup_planning.pycomment that made the intentional duplication greppable; the fix stayed in lockstep without surprise. - AC verification was almost entirely automatable via short tmp-dir scripts — no Obsidian required.
Friction and automation gaps
Section titled “Friction and automation gaps”plugin/scripts/run_quality_checks.pyfailed at the schema-validation pre-flight because the project’ssdlc.yamlcontains apr_check:block that the schema (plugin/schemas/sdlc-yaml.schema.json) doesn’t declare. This blocked Step 7 entirely, even though every declared quality_check verb passes when invoked directly. The schema was added by commit21d3284and thepr_check:block bye463491; the schema was never extended to cover the new key. The same drift will block every/sdlc:task-workrun until the schema is widened (or the convention doc walked back). → spawn a follow-up to addpr_checktoplugin/schemas/sdlc-yaml.schema.json(mirror the shape documented inplugin/conventions/sdlc-yaml.md). → T-DN76-pr-check-schema-promote- The
pluralize()helper is now duplicated in two files (plugin/skills/setup/setup_planning.pyandplugin/scripts/audit_entities.py) — both carry anIRREGULAR_PLURALSmap that must be edited in lockstep. The PRINCIPLES.md “co-locate first, promote when shared” rule says promotion happens once a second caller appears, which is now the case. → spawn a follow-up to promotepluralize()+IRREGULAR_PLURALSto a shared module (e.g.plugin/scripts/entity_naming.py) and have both callers import it. → T-8XI6-promote-pluralize-to-shared-helper site/scripts/regen.mjsrewrote 21 site files (660 insertions, 139 deletions) when run to refresh the three updated plugin sources — most of those edits were unrelated drift from upstream changes that hadn’t been re-genned. Had to revert all site changes to keep the PR scoped to backlog-naming. → either run regen as a separate maintenance task that lands its own commit, or have regen support a--filterflag so a single-skill update doesn’t sweep in unrelated drift. → T-ETML-decouple-regen-from-task-work-prs
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-DN76-pr-check-schema-promote — linked (existing task already covered this exact gap)
- T-8XI6-promote-pluralize-to-shared-helper — created (new task)
- T-ETML-decouple-regen-from-task-work-prs — linked (existing task already covered this exact gap)