Skip to content

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 backlog vs backlogs directory inconsistency surfaced again: pluralize("backlog") produces backlogs but real backlog content lives in docs/planning/backlog/. Worked around in write_bases with 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.

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.

  1. Add a small irregular-plural map (or an “already-plural” set) to pluralize() in plugin/skills/setup/setup_planning.py. Initial entry: "backlog" -> "backlog". Decide whether to keep the helper inline in setup_planning.py or promote it to a shared module — leave inline if no second caller exists yet.
  2. Delete the carve-out branch in write_bases() that special-cases entity == "backlog". Verify by re-running setup_planning.py --obsidian --dry-run on a fresh tmp dir and confirming the destination is still docs/planning/backlog/backlog.base.
  3. Re-run /sdlc:setup end-to-end on a fresh project to confirm AC-5 from the originating task still holds (no .base files when --obsidian is absent; correct destinations when it’s present).
  • plugin/skills/setup/setup_planning.py — extend pluralize() with an irregular-plural carve-out; remove the inline entity == "backlog" branch from write_bases().
  • plugin/entities/backlog/base.yaml — update the file.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 keep backlog/ singular is consistent with pluralize("backlog") == "backlog").
  • plugin/scripts/new_backlog.py — currently writes new backlog files to docs/planning/backlogs/ (plural) at line 155; realign to the same singular/plural decision so all callers agree.
  • AC-1: pluralize("backlog") == "backlog" (assert in a smoke test or quick python -c).
  • AC-2: setup_planning.py --obsidian --dry-run in a fresh tmp project reports docs/planning/backlog/backlog.base as the backlog destination with no special-casing in the calling code.
  • AC-3: write_bases() contains no if 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. no backlogs/backlog.base or backlog/backlogs.base mismatch).
  • AC-6 (absorbed): After a fresh /sdlc:setup --obsidian run on a clean project, only one of docs/planning/backlog/ or docs/planning/backlogs/ exists — no orphan parallel directory.
  • AC-7 (absorbed): new_backlog.py writes new files to the same directory the entity files live in.
  • Renaming the plugin/entities/backlog/ directory to backlogitem/ (the heavier alternative noted in the post-mortem).
  • Generalizing pluralize() into a full English-plural library — the carve-out map is enough.
  • none

Spawned by /sdlc:task-work post-mortem of T-781K-add-obsidian-bases-setup-flag on 2026-05-20.

Captured by /sdlc:task-work on 2026-05-21. PR: pending.

  • AC-1: agent-manual — python -c imported setup_planning.pluralize; asserted pluralize("backlog") == "backlog" and that task/bug/epic/feature/milestone/roadmap/story still pluralize correctly.
  • AC-2: agent-manual — ran setup_planning.py --obsidian --dry-run in /tmp/pluralize-ac2-test; output included would write: docs/planning/backlog/backlog.base with no special-casing in the calling code (carve-out branch deleted).
  • AC-3: auto — command grep for entity == "backlog" / singular == "backlog" / == 'backlog' in setup_planning.py returned no matches.
  • AC-4: agent-manual — re-ran the originating task’s AC-1/AC-3/AC-5 invariants against a fresh tmp dir; .base files land in docs/planning/{tasks,epics,milestones,backlog}/, second run without --force leaves mtime unchanged, run without --obsidian writes zero .base files.
  • AC-5: auto — plugin/entities/backlog/base.yaml already references file.inFolder("planning/backlog"), matching the destination docs/planning/backlog/backlog.base written by AC-2’s dry-run.
  • AC-6: agent-manual — setup_planning.py --obsidian in /tmp/pluralize-ac6-test; ls docs/planning/ showed only backlog/ (no orphan backlogs/).
  • AC-7: agent-manual — new_backlog.py test-slug --project-root . in /tmp/pluralize-ac7-test; new file landed at docs/planning/backlog/test-slug.md.
  • Two pluralize() copies (setup_planning.py + audit_entities.py) — the second was easy to find because the audit copy carries a # mirror setup_planning.py comment 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.
  • plugin/scripts/run_quality_checks.py failed at the schema-validation pre-flight because the project’s sdlc.yaml contains a pr_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 commit 21d3284 and the pr_check: block by e463491; the schema was never extended to cover the new key. The same drift will block every /sdlc:task-work run until the schema is widened (or the convention doc walked back). → spawn a follow-up to add pr_check to plugin/schemas/sdlc-yaml.schema.json (mirror the shape documented in plugin/conventions/sdlc-yaml.md). → T-DN76-pr-check-schema-promote
  • The pluralize() helper is now duplicated in two files (plugin/skills/setup/setup_planning.py and plugin/scripts/audit_entities.py) — both carry an IRREGULAR_PLURALS map 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 promote pluralize() + IRREGULAR_PLURALS to 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.mjs rewrote 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 --filter flag so a single-skill update doesn’t sweep in unrelated drift. → T-ETML-decouple-regen-from-task-work-prs

← Back to Tasks