@pennyfarthing/core 10.0.5 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -22
- package/package.json +17 -11
- package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor-legacy.test.js +24 -0
- package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +346 -13
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/e2e-fresh-install.test.js +1 -1
- package/packages/core/dist/cli/commands/e2e-fresh-install.test.js.map +1 -1
- package/packages/core/dist/cli/commands/e2e-upgrade.test.js +1 -1
- package/packages/core/dist/cli/commands/e2e-upgrade.test.js.map +1 -1
- package/packages/core/dist/cli/commands/hooks-consolidation.test.js +2 -2
- package/packages/core/dist/cli/commands/hooks-consolidation.test.js.map +1 -1
- package/packages/core/dist/cli/commands/init-consolidation.test.js.map +1 -1
- package/packages/core/dist/cli/commands/init.d.ts +7 -0
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +41 -8
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/uninstall.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/uninstall.js +24 -13
- package/packages/core/dist/cli/commands/uninstall.js.map +1 -1
- package/packages/core/dist/cli/commands/update-consolidation.test.js +0 -10
- package/packages/core/dist/cli/commands/update-consolidation.test.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +26 -0
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/index.js +1 -1
- package/packages/core/dist/cli/index.js.map +1 -1
- package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
- package/packages/core/dist/cli/theme-maker.test.js +64 -115
- package/packages/core/dist/cli/theme-maker.test.js.map +1 -1
- package/packages/core/dist/cli/utils/python.d.ts +22 -0
- package/packages/core/dist/cli/utils/python.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/python.js +102 -0
- package/packages/core/dist/cli/utils/python.js.map +1 -0
- package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/settings.js +10 -0
- package/packages/core/dist/cli/utils/settings.js.map +1 -1
- package/packages/core/dist/index.d.ts +1 -1
- package/packages/core/dist/index.d.ts.map +1 -1
- package/packages/core/dist/index.js +2 -2
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/plugins/plugin-discovery.d.ts +116 -0
- package/packages/core/dist/plugins/plugin-discovery.d.ts.map +1 -0
- package/packages/core/dist/plugins/plugin-discovery.js +165 -0
- package/packages/core/dist/plugins/plugin-discovery.js.map +1 -0
- package/packages/core/dist/plugins/plugin-discovery.test.d.ts +22 -0
- package/packages/core/dist/plugins/plugin-discovery.test.d.ts.map +1 -0
- package/packages/core/dist/plugins/plugin-discovery.test.js +498 -0
- package/packages/core/dist/plugins/plugin-discovery.test.js.map +1 -0
- package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
- package/packages/core/dist/workflow/context-watch.d.ts +80 -0
- package/packages/core/dist/workflow/context-watch.d.ts.map +1 -0
- package/packages/core/dist/workflow/context-watch.js +235 -0
- package/packages/core/dist/workflow/context-watch.js.map +1 -0
- package/packages/core/dist/workflow/context-watch.test.d.ts +1 -0
- package/packages/core/dist/workflow/context-watch.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/context-watch.test.js +746 -0
- package/packages/core/dist/workflow/context-watch.test.js.map +1 -0
- package/packages/core/dist/workflow/file-watch.d.ts +82 -0
- package/packages/core/dist/workflow/file-watch.d.ts.map +1 -0
- package/packages/core/dist/workflow/file-watch.js +198 -0
- package/packages/core/dist/workflow/file-watch.js.map +1 -0
- package/packages/core/dist/workflow/file-watch.test.d.ts +21 -0
- package/packages/core/dist/workflow/file-watch.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/file-watch.test.js +469 -0
- package/packages/core/dist/workflow/file-watch.test.js.map +1 -0
- package/packages/core/dist/workflow/observation-writer.d.ts +79 -0
- package/packages/core/dist/workflow/observation-writer.d.ts.map +1 -0
- package/packages/core/dist/workflow/observation-writer.js +97 -0
- package/packages/core/dist/workflow/observation-writer.js.map +1 -0
- package/packages/core/dist/workflow/observation-writer.test.d.ts +18 -0
- package/packages/core/dist/workflow/observation-writer.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/observation-writer.test.js +424 -0
- package/packages/core/dist/workflow/observation-writer.test.js.map +1 -0
- package/packages/core/dist/workflow/story-workflow-routing.test.js +4 -2
- package/packages/core/dist/workflow/story-workflow-routing.test.js.map +1 -1
- package/packages/core/dist/workflow/tandem-lifecycle.d.ts +117 -0
- package/packages/core/dist/workflow/tandem-lifecycle.d.ts.map +1 -0
- package/packages/core/dist/workflow/tandem-lifecycle.js +186 -0
- package/packages/core/dist/workflow/tandem-lifecycle.js.map +1 -0
- package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts +16 -0
- package/packages/core/dist/workflow/tandem-lifecycle.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/tandem-lifecycle.test.js +531 -0
- package/packages/core/dist/workflow/tandem-lifecycle.test.js.map +1 -0
- package/packages/core/dist/workflow/tool-watch.d.ts +68 -0
- package/packages/core/dist/workflow/tool-watch.d.ts.map +1 -0
- package/packages/core/dist/workflow/tool-watch.js +166 -0
- package/packages/core/dist/workflow/tool-watch.js.map +1 -0
- package/packages/core/dist/workflow/tool-watch.test.d.ts +18 -0
- package/packages/core/dist/workflow/tool-watch.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/tool-watch.test.js +718 -0
- package/packages/core/dist/workflow/tool-watch.test.js.map +1 -0
- package/packages/core/dist/workflow/workflow-migration.test.js +8 -4
- package/packages/core/dist/workflow/workflow-migration.test.js.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.d.ts +7 -0
- package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.js +44 -0
- package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.test.js +192 -0
- package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
- package/pennyfarthing-dist/agents/README.md +1 -3
- package/pennyfarthing-dist/agents/architect.md +0 -6
- package/pennyfarthing-dist/agents/devops.md +0 -6
- package/pennyfarthing-dist/agents/handoff.md +18 -3
- package/pennyfarthing-dist/agents/orchestrator.md +0 -6
- package/pennyfarthing-dist/agents/pm.md +0 -6
- package/pennyfarthing-dist/agents/sm-finish.md +1 -1
- package/pennyfarthing-dist/agents/sm-handoff.md +27 -4
- package/pennyfarthing-dist/agents/sm.md +11 -11
- package/pennyfarthing-dist/agents/tandem-backseat.md +119 -0
- package/pennyfarthing-dist/commands/architect.md +11 -3
- package/pennyfarthing-dist/commands/close-epic.md +24 -131
- package/pennyfarthing-dist/commands/create-theme.md +14 -24
- package/pennyfarthing-dist/commands/dev.md +11 -3
- package/pennyfarthing-dist/commands/devops.md +11 -3
- package/pennyfarthing-dist/commands/health-check.md +1 -3
- package/pennyfarthing-dist/commands/help.md +8 -12
- package/pennyfarthing-dist/commands/list-themes.md +14 -16
- package/pennyfarthing-dist/commands/orchestrator.md +11 -3
- package/pennyfarthing-dist/commands/parallel-work.md +1 -3
- package/pennyfarthing-dist/commands/pm.md +11 -3
- package/pennyfarthing-dist/commands/prime.md +6 -6
- package/pennyfarthing-dist/commands/reviewer.md +11 -3
- package/pennyfarthing-dist/commands/run-ci.md +1 -1
- package/pennyfarthing-dist/commands/set-theme.md +14 -51
- package/pennyfarthing-dist/commands/setup.md +5 -1
- package/pennyfarthing-dist/commands/show-theme.md +14 -16
- package/pennyfarthing-dist/commands/sm.md +11 -3
- package/pennyfarthing-dist/commands/tea.md +11 -3
- package/pennyfarthing-dist/commands/tech-writer.md +11 -3
- package/pennyfarthing-dist/commands/theme-maker.md +14 -671
- package/pennyfarthing-dist/commands/theme.md +95 -0
- package/pennyfarthing-dist/commands/ux-designer.md +11 -3
- package/pennyfarthing-dist/commands/work.md +3 -5
- package/pennyfarthing-dist/guides/agent-behavior.md +62 -6
- package/pennyfarthing-dist/guides/agent-coordination.md +11 -13
- package/pennyfarthing-dist/guides/agent-template-tactical.md +2 -3
- package/pennyfarthing-dist/guides/bikelane.md +3 -2
- package/pennyfarthing-dist/guides/command-tag-taxonomy.md +212 -0
- package/pennyfarthing-dist/guides/hooks.md +5 -5
- package/pennyfarthing-dist/guides/patterns/fan-out-fan-in-pattern.md +3 -3
- package/pennyfarthing-dist/guides/patterns/helper-delegation-pattern.md +9 -59
- package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -5
- package/pennyfarthing-dist/guides/prime.md +2 -2
- package/pennyfarthing-dist/guides/scale-levels.md +4 -6
- package/pennyfarthing-dist/guides/skill-schema.md +4 -4
- package/pennyfarthing-dist/guides/tandem-protocol.md +158 -0
- package/pennyfarthing-dist/personas/themes/discworld.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/fifth-element.yaml +295 -0
- package/pennyfarthing-dist/scripts/README.md +1 -1
- package/pennyfarthing-dist/scripts/core/agent-session.sh +6 -2
- package/pennyfarthing-dist/scripts/core/check-context.sh +0 -0
- package/pennyfarthing-dist/scripts/core/handoff-marker.sh +0 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
- package/pennyfarthing-dist/scripts/core/prime.sh +8 -10
- package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +0 -0
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +0 -0
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +8 -6
- package/pennyfarthing-dist/scripts/git/release.sh +0 -0
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +0 -0
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +131 -54
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +32 -15
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +4 -3
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +11 -5
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
- package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/README.md +1 -1
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
- package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +50 -8
- package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +1 -2
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
- package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +5 -5
- package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +3 -79
- package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/theme/README.md +1 -1
- package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -1
- package/pennyfarthing-dist/scripts/workflow/README.md +2 -2
- package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +10 -144
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +0 -0
- package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +2 -2
- package/pennyfarthing-dist/skills/skill-registry.schema.json +8 -0
- package/pennyfarthing-dist/skills/skill-registry.yaml +21 -17
- package/pennyfarthing-dist/skills/sprint/skill.md +25 -2
- package/pennyfarthing-dist/skills/story/scripts/create-story.sh +0 -0
- package/pennyfarthing-dist/skills/story/scripts/size-story.sh +0 -0
- package/pennyfarthing-dist/skills/story/scripts/story-template.sh +0 -0
- package/pennyfarthing-dist/skills/theme/skill.md +290 -75
- package/pennyfarthing-dist/skills/theme-creation/SKILL.md +23 -166
- package/pennyfarthing-dist/skills/workflow/scripts/list-workflows.sh +0 -0
- package/pennyfarthing-dist/skills/workflow/scripts/resume-workflow.sh +0 -0
- package/pennyfarthing-dist/skills/workflow/scripts/show-workflow.sh +0 -0
- package/pennyfarthing-dist/skills/workflow/scripts/start-workflow.sh +0 -0
- package/pennyfarthing-dist/skills/workflow/scripts/workflow-status.sh +0 -0
- package/pennyfarthing-dist/skills/workflow/skill.md +27 -4
- package/pennyfarthing-dist/templates/agent-scopes.yaml.template +0 -11
- package/pennyfarthing-dist/templates/auto-load-sm.sh.template +14 -0
- package/pennyfarthing-dist/templates/settings.local.json.template +9 -0
- package/pennyfarthing-dist/workflows/2party-tdd.yaml +399 -0
- package/pennyfarthing-dist/workflows/architecture/workflow.yaml +65 -0
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +70 -0
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +41 -24
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +61 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bellmode_hook.py +202 -47
- package/pennyfarthing_scripts/brownfield/__init__.py +6 -6
- package/pennyfarthing_scripts/brownfield/__main__.py +1 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/cli.py +0 -1
- package/pennyfarthing_scripts/brownfield/discover.py +1 -2
- package/pennyfarthing_scripts/cli.py +23 -3
- package/pennyfarthing_scripts/codemarkers/__init__.py +23 -0
- package/pennyfarthing_scripts/codemarkers/__main__.py +6 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/analyze.py +501 -0
- package/pennyfarthing_scripts/codemarkers/cli.py +179 -0
- package/pennyfarthing_scripts/codemarkers/formatters.py +88 -0
- package/pennyfarthing_scripts/codemarkers/models.py +60 -0
- package/pennyfarthing_scripts/common/__init__.py +8 -9
- package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +1 -1
- package/pennyfarthing_scripts/complexity/__init__.py +15 -0
- package/pennyfarthing_scripts/complexity/__main__.py +6 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/analyze.py +207 -0
- package/pennyfarthing_scripts/complexity/cli.py +82 -0
- package/pennyfarthing_scripts/complexity/formatters.py +64 -0
- package/pennyfarthing_scripts/complexity/models.py +32 -0
- package/pennyfarthing_scripts/context.py +14 -15
- package/pennyfarthing_scripts/deadcode/__init__.py +6 -0
- package/pennyfarthing_scripts/deadcode/__main__.py +6 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/analyze.py +322 -0
- package/pennyfarthing_scripts/deadcode/cli.py +163 -0
- package/pennyfarthing_scripts/deadcode/formatters.py +106 -0
- package/pennyfarthing_scripts/deadcode/models.py +54 -0
- package/pennyfarthing_scripts/dependencies/__init__.py +20 -0
- package/pennyfarthing_scripts/dependencies/__main__.py +5 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/analyze.py +155 -0
- package/pennyfarthing_scripts/dependencies/cli.py +76 -0
- package/pennyfarthing_scripts/dependencies/formatters.py +63 -0
- package/pennyfarthing_scripts/dependencies/models.py +39 -0
- package/pennyfarthing_scripts/git/__init__.py +5 -5
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +3 -2
- package/pennyfarthing_scripts/git/status_all.py +1 -1
- package/pennyfarthing_scripts/healthscore/__init__.py +21 -0
- package/pennyfarthing_scripts/healthscore/__main__.py +14 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/analyze.py +591 -0
- package/pennyfarthing_scripts/healthscore/cli.py +80 -0
- package/pennyfarthing_scripts/healthscore/formatters.py +46 -0
- package/pennyfarthing_scripts/healthscore/models.py +43 -0
- package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing_scripts/hooks.py +8 -11
- package/pennyfarthing_scripts/hotspots/__init__.py +6 -6
- package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/analyze.py +155 -14
- package/pennyfarthing_scripts/hotspots/cli.py +12 -10
- package/pennyfarthing_scripts/hotspots/models.py +0 -1
- package/pennyfarthing_scripts/jira/__init__.py +15 -17
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/bidirectional.py +44 -18
- package/pennyfarthing_scripts/jira/claim.py +21 -0
- package/pennyfarthing_scripts/jira/cli.py +6 -3
- package/pennyfarthing_scripts/jira/client.py +32 -4
- package/pennyfarthing_scripts/jira/create.py +45 -1
- package/pennyfarthing_scripts/jira/epic.py +3 -2
- package/pennyfarthing_scripts/jira/reconcile.py +0 -1
- package/pennyfarthing_scripts/jira/story.py +2 -0
- package/pennyfarthing_scripts/jira/sync.py +1 -1
- package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/skill.py +0 -1
- package/pennyfarthing_scripts/migration/step.py +0 -1
- package/pennyfarthing_scripts/migration/validate.py +8 -5
- package/pennyfarthing_scripts/patch_mode.py +2 -2
- package/pennyfarthing_scripts/preflight/__init__.py +1 -1
- package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/finish.py +0 -1
- package/pennyfarthing_scripts/pretooluse_hook.py +6 -7
- package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/cli.py +5 -1
- package/pennyfarthing_scripts/prime/loader.py +2 -3
- package/pennyfarthing_scripts/prime/persona.py +2 -1
- package/pennyfarthing_scripts/prime/tiers.py +4 -4
- package/pennyfarthing_scripts/schema_validation_hook.py +2 -3
- package/pennyfarthing_scripts/sprint/__init__.py +10 -12
- package/pennyfarthing_scripts/sprint/__main__.py +2 -2
- package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive.py +0 -1
- package/pennyfarthing_scripts/sprint/archive_epic.py +198 -97
- package/pennyfarthing_scripts/sprint/cli.py +62 -46
- package/pennyfarthing_scripts/sprint/epic_add.py +8 -1
- package/pennyfarthing_scripts/sprint/import_epic.py +42 -18
- package/pennyfarthing_scripts/sprint/loader.py +6 -0
- package/pennyfarthing_scripts/sprint/status.py +1 -2
- package/pennyfarthing_scripts/sprint/story_add.py +202 -27
- package/pennyfarthing_scripts/sprint/story_finish.py +209 -0
- package/pennyfarthing_scripts/sprint/story_update.py +11 -3
- package/pennyfarthing_scripts/sprint/validate_cmd.py +0 -1
- package/pennyfarthing_scripts/sprint/validator.py +120 -6
- package/pennyfarthing_scripts/sprint/work.py +28 -7
- package/pennyfarthing_scripts/sprint/yaml_io.py +10 -2
- package/pennyfarthing_scripts/story/__init__.py +14 -16
- package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/size.py +0 -1
- package/pennyfarthing_scripts/story/template.py +0 -1
- package/pennyfarthing_scripts/swebench.py +1 -2
- package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/conftest.py +1 -2
- package/pennyfarthing_scripts/tests/test_brownfield.py +10 -13
- package/pennyfarthing_scripts/tests/test_cli_modules.py +0 -4
- package/pennyfarthing_scripts/tests/test_codemarkers.py +687 -0
- package/pennyfarthing_scripts/tests/test_common.py +9 -4
- package/pennyfarthing_scripts/tests/test_epic_shard_validation.py +699 -0
- package/pennyfarthing_scripts/tests/test_git_utils.py +10 -13
- package/pennyfarthing_scripts/tests/test_healthscore.py +516 -0
- package/pennyfarthing_scripts/tests/test_jira_package.py +0 -3
- package/pennyfarthing_scripts/tests/test_package_structure.py +3 -16
- package/pennyfarthing_scripts/tests/test_patch_mode.py +7 -11
- package/pennyfarthing_scripts/tests/test_prime.py +39 -21
- package/pennyfarthing_scripts/tests/test_sprint_package.py +3 -8
- package/pennyfarthing_scripts/tests/test_sprint_validator.py +53 -5
- package/pennyfarthing_scripts/tests/test_story_add.py +3 -7
- package/pennyfarthing_scripts/tests/test_story_package.py +0 -3
- package/pennyfarthing_scripts/tests/test_story_update.py +5 -10
- package/pennyfarthing_scripts/tests/test_tiers.py +18 -17
- package/pennyfarthing_scripts/tests/test_token_counting.py +19 -13
- package/pennyfarthing_scripts/tests/test_validate_cmd.py +2 -7
- package/pennyfarthing_scripts/tests/test_workflow_check.py +0 -2
- package/pennyfarthing_scripts/tests/test_yaml_io.py +0 -3
- package/pennyfarthing_scripts/theme/__init__.py +5 -0
- package/pennyfarthing_scripts/theme/__main__.py +6 -0
- package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/cli.py +287 -0
- package/pennyfarthing_scripts/validate/__init__.py +21 -0
- package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__init__.py +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/agent.py +239 -0
- package/pennyfarthing_scripts/validate/adapters/schema.py +30 -0
- package/pennyfarthing_scripts/validate/adapters/skill_command.py +292 -0
- package/pennyfarthing_scripts/validate/adapters/sprint.py +69 -0
- package/pennyfarthing_scripts/validate/adapters/workflow.py +320 -0
- package/pennyfarthing_scripts/validate/cli.py +141 -0
- package/pennyfarthing_scripts/welcome_hook.py +2 -3
- package/pennyfarthing_scripts/workflow.py +3 -3
- package/scripts/README.md +41 -0
- package/pennyfarthing-dist/agents/workflow-status-check.md +0 -96
- package/pennyfarthing-dist/commands/benchmark-control.md +0 -69
- package/pennyfarthing-dist/commands/benchmark.md +0 -485
- package/pennyfarthing-dist/commands/job-fair.md +0 -102
- package/pennyfarthing-dist/commands/solo.md +0 -447
- package/pennyfarthing-dist/guides/benchmarks.md +0 -62
- package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +0 -59
- package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +0 -220
- package/pennyfarthing-dist/scripts/test/swebench-judge.py +0 -374
- package/pennyfarthing-dist/scripts/test/test-cache.sh +0 -165
- package/pennyfarthing-dist/scripts/test/test-setup.sh +0 -337
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +0 -13
- package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +0 -402
- package/pennyfarthing-dist/scripts/theme/update-theme-tiers.sh +0 -97
- package/pennyfarthing-dist/skills/finalize-run/SKILL.md +0 -261
- package/pennyfarthing-dist/skills/judge/SKILL.md +0 -644
- package/pennyfarthing-dist/skills/persona-benchmark/SKILL.md +0 -187
- package/pennyfarthing-dist/workflows/dev-story/checklist.md +0 -80
- package/pennyfarthing-dist/workflows/dev-story/instructions.xml +0 -410
- package/pennyfarthing-dist/workflows/dev-story/workflow.yaml +0 -50
- package/pennyfarthing-dist/workflows/quick-spec/steps/step-01-understand.md +0 -201
- package/pennyfarthing-dist/workflows/quick-spec/steps/step-02-investigate.md +0 -156
- package/pennyfarthing-dist/workflows/quick-spec/steps/step-03-generate.md +0 -140
- package/pennyfarthing-dist/workflows/quick-spec/steps/step-04-review.md +0 -203
- package/pennyfarthing-dist/workflows/quick-spec/tech-spec-template.md +0 -74
- package/pennyfarthing-dist/workflows/quick-spec/workflow.yaml +0 -27
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"""Tests for epic shard write-time validation and reference integrity.
|
|
2
|
+
|
|
3
|
+
Story: MSSCI-14734 / 91-24 - Sprint shard write-time validation
|
|
4
|
+
ADR: ADR-0022 - Sprint Shard Validation and Reference Integrity
|
|
5
|
+
|
|
6
|
+
Tests cover all six acceptance criteria:
|
|
7
|
+
AC1: validate_epic_shard() rejects epics missing id, title, status, or stories
|
|
8
|
+
AC2: _get_epic_ref() strips epic- prefix from IDs to prevent double-prefix filenames
|
|
9
|
+
AC3: epic_add, epic_promote, jira_create_epic, import_epic all call validator before write
|
|
10
|
+
AC4: _merge_epic_shards() emits warning for unresolvable refs (not silent skip)
|
|
11
|
+
AC5: Jira create epic checks for existing epic with same title before creating
|
|
12
|
+
AC6: Existing validator tests pass; new tests cover all five validation rules
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import warnings
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
from pennyfarthing_scripts.sprint.validator import (
|
|
23
|
+
REQUIRED_EPIC_SHARD_FIELDS,
|
|
24
|
+
REQUIRED_SHARD_STORY_FIELDS,
|
|
25
|
+
ValidationResult,
|
|
26
|
+
validate_epic_shard,
|
|
27
|
+
validate_sprint_file,
|
|
28
|
+
)
|
|
29
|
+
from pennyfarthing_scripts.sprint.yaml_io import _get_epic_ref
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# Fixtures
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def valid_epic_shard() -> dict[str, Any]:
|
|
38
|
+
"""A minimal valid epic shard dict."""
|
|
39
|
+
return {
|
|
40
|
+
"id": "94",
|
|
41
|
+
"title": "Epic: Cross-File Validation",
|
|
42
|
+
"status": "backlog",
|
|
43
|
+
"stories": [
|
|
44
|
+
{
|
|
45
|
+
"id": "94-1",
|
|
46
|
+
"title": "First story",
|
|
47
|
+
"points": 3,
|
|
48
|
+
"status": "backlog",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def valid_epic_shard_with_jira() -> dict[str, Any]:
|
|
56
|
+
"""A valid epic shard with a Jira key."""
|
|
57
|
+
return {
|
|
58
|
+
"id": "94",
|
|
59
|
+
"title": "Epic: Validation Pipeline",
|
|
60
|
+
"status": "in_progress",
|
|
61
|
+
"jira": "MSSCI-14659",
|
|
62
|
+
"stories": [
|
|
63
|
+
{
|
|
64
|
+
"id": "94-1",
|
|
65
|
+
"title": "Wire up validation",
|
|
66
|
+
"points": 5,
|
|
67
|
+
"status": "done",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# AC1: validate_epic_shard() rejects epics missing required fields
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestValidateEpicShardRequiredFields:
|
|
79
|
+
"""validate_epic_shard enforces id, title, status, stories are required."""
|
|
80
|
+
|
|
81
|
+
def test_valid_shard_passes(self, valid_epic_shard: dict[str, Any]) -> None:
|
|
82
|
+
"""A complete, well-formed shard should pass validation."""
|
|
83
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
84
|
+
|
|
85
|
+
assert result.valid is True
|
|
86
|
+
assert len(result.errors) == 0
|
|
87
|
+
|
|
88
|
+
def test_constants_defined(self) -> None:
|
|
89
|
+
"""Required field constants should exist and match ADR-0022."""
|
|
90
|
+
assert REQUIRED_EPIC_SHARD_FIELDS == {"id", "title", "status", "stories"}
|
|
91
|
+
assert REQUIRED_SHARD_STORY_FIELDS == {"id", "title", "points", "status"}
|
|
92
|
+
|
|
93
|
+
def test_missing_id_fails(self, valid_epic_shard: dict[str, Any]) -> None:
|
|
94
|
+
"""Shard without 'id' should fail validation."""
|
|
95
|
+
del valid_epic_shard["id"]
|
|
96
|
+
|
|
97
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
98
|
+
|
|
99
|
+
assert result.valid is False
|
|
100
|
+
assert any("id" in e.message.lower() for e in result.errors)
|
|
101
|
+
|
|
102
|
+
def test_missing_title_fails(self, valid_epic_shard: dict[str, Any]) -> None:
|
|
103
|
+
"""Shard without 'title' should fail validation."""
|
|
104
|
+
del valid_epic_shard["title"]
|
|
105
|
+
|
|
106
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
107
|
+
|
|
108
|
+
assert result.valid is False
|
|
109
|
+
assert any("title" in e.message.lower() for e in result.errors)
|
|
110
|
+
|
|
111
|
+
def test_missing_status_fails(self, valid_epic_shard: dict[str, Any]) -> None:
|
|
112
|
+
"""Shard without 'status' should fail validation."""
|
|
113
|
+
del valid_epic_shard["status"]
|
|
114
|
+
|
|
115
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
116
|
+
|
|
117
|
+
assert result.valid is False
|
|
118
|
+
assert any("status" in e.message.lower() for e in result.errors)
|
|
119
|
+
|
|
120
|
+
def test_missing_stories_fails(self, valid_epic_shard: dict[str, Any]) -> None:
|
|
121
|
+
"""Shard without 'stories' should fail validation."""
|
|
122
|
+
del valid_epic_shard["stories"]
|
|
123
|
+
|
|
124
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
125
|
+
|
|
126
|
+
assert result.valid is False
|
|
127
|
+
assert any("stories" in e.message.lower() for e in result.errors)
|
|
128
|
+
|
|
129
|
+
def test_empty_dict_fails_all_fields(self) -> None:
|
|
130
|
+
"""Empty dict should report all four missing required fields."""
|
|
131
|
+
result = validate_epic_shard({})
|
|
132
|
+
|
|
133
|
+
assert result.valid is False
|
|
134
|
+
assert len(result.errors) >= 4
|
|
135
|
+
|
|
136
|
+
def test_stories_must_be_list(self, valid_epic_shard: dict[str, Any]) -> None:
|
|
137
|
+
"""'stories' field must be a list, not a string or dict."""
|
|
138
|
+
valid_epic_shard["stories"] = "not a list"
|
|
139
|
+
|
|
140
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
141
|
+
|
|
142
|
+
assert result.valid is False
|
|
143
|
+
assert any("list" in e.message.lower() for e in result.errors)
|
|
144
|
+
|
|
145
|
+
def test_story_missing_required_fields(
|
|
146
|
+
self, valid_epic_shard: dict[str, Any]
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Stories within shard must have id, title, points, status."""
|
|
149
|
+
valid_epic_shard["stories"] = [{"id": "94-1"}] # Missing title, points, status
|
|
150
|
+
|
|
151
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
152
|
+
|
|
153
|
+
assert result.valid is False
|
|
154
|
+
error_msgs = " ".join(e.message for e in result.errors)
|
|
155
|
+
assert "title" in error_msgs.lower()
|
|
156
|
+
assert "points" in error_msgs.lower()
|
|
157
|
+
assert "status" in error_msgs.lower()
|
|
158
|
+
|
|
159
|
+
def test_duplicate_story_ids_within_shard_fails(
|
|
160
|
+
self, valid_epic_shard: dict[str, Any]
|
|
161
|
+
) -> None:
|
|
162
|
+
"""No duplicate story IDs within a single shard."""
|
|
163
|
+
valid_epic_shard["stories"] = [
|
|
164
|
+
{"id": "94-1", "title": "Story A", "points": 3, "status": "backlog"},
|
|
165
|
+
{"id": "94-1", "title": "Story B", "points": 2, "status": "done"},
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
169
|
+
|
|
170
|
+
assert result.valid is False
|
|
171
|
+
assert any("duplicate" in e.message.lower() for e in result.errors)
|
|
172
|
+
|
|
173
|
+
def test_valid_jira_key_passes(
|
|
174
|
+
self, valid_epic_shard_with_jira: dict[str, Any]
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Valid MSSCI-NNNNN Jira key should pass."""
|
|
177
|
+
result = validate_epic_shard(valid_epic_shard_with_jira)
|
|
178
|
+
|
|
179
|
+
assert result.valid is True
|
|
180
|
+
|
|
181
|
+
def test_invalid_jira_key_fails(
|
|
182
|
+
self, valid_epic_shard: dict[str, Any]
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Invalid Jira key format should fail."""
|
|
185
|
+
valid_epic_shard["jira"] = "INVALID-KEY"
|
|
186
|
+
|
|
187
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
188
|
+
|
|
189
|
+
assert result.valid is False
|
|
190
|
+
assert any("jira" in e.message.lower() for e in result.errors)
|
|
191
|
+
|
|
192
|
+
def test_jira_key_wrong_project_fails(
|
|
193
|
+
self, valid_epic_shard: dict[str, Any]
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Jira key from wrong project should fail."""
|
|
196
|
+
valid_epic_shard["jira"] = "PROJ-12345"
|
|
197
|
+
|
|
198
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
199
|
+
|
|
200
|
+
assert result.valid is False
|
|
201
|
+
|
|
202
|
+
def test_empty_stories_list_passes(
|
|
203
|
+
self, valid_epic_shard: dict[str, Any]
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Empty stories list is valid (epic exists but has no stories yet)."""
|
|
206
|
+
valid_epic_shard["stories"] = []
|
|
207
|
+
|
|
208
|
+
result = validate_epic_shard(valid_epic_shard)
|
|
209
|
+
|
|
210
|
+
assert result.valid is True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# =============================================================================
|
|
214
|
+
# AC1 continued: epic- prefix rejection (ADR-0022 root cause fix)
|
|
215
|
+
# =============================================================================
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestEpicPrefixRejection:
|
|
219
|
+
"""validate_epic_shard rejects IDs starting with 'epic-' prefix."""
|
|
220
|
+
|
|
221
|
+
def test_epic_prefix_in_id_rejected(self) -> None:
|
|
222
|
+
"""ID 'epic-94' should fail — reference prefix baked into value."""
|
|
223
|
+
shard = {
|
|
224
|
+
"id": "epic-94",
|
|
225
|
+
"title": "Test",
|
|
226
|
+
"status": "backlog",
|
|
227
|
+
"stories": [],
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
result = validate_epic_shard(shard)
|
|
231
|
+
|
|
232
|
+
assert result.valid is False
|
|
233
|
+
assert any("epic-" in e.message for e in result.errors)
|
|
234
|
+
|
|
235
|
+
def test_numeric_id_accepted(self) -> None:
|
|
236
|
+
"""Numeric ID '94' should pass."""
|
|
237
|
+
shard = {
|
|
238
|
+
"id": "94",
|
|
239
|
+
"title": "Test",
|
|
240
|
+
"status": "backlog",
|
|
241
|
+
"stories": [],
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
result = validate_epic_shard(shard)
|
|
245
|
+
|
|
246
|
+
assert result.valid is True
|
|
247
|
+
|
|
248
|
+
def test_jira_key_as_id_accepted(self) -> None:
|
|
249
|
+
"""Jira key as ID should pass (e.g., 'MSSCI-14510')."""
|
|
250
|
+
shard = {
|
|
251
|
+
"id": "MSSCI-14510",
|
|
252
|
+
"title": "Test",
|
|
253
|
+
"status": "backlog",
|
|
254
|
+
"stories": [],
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
result = validate_epic_shard(shard)
|
|
258
|
+
|
|
259
|
+
assert result.valid is True
|
|
260
|
+
|
|
261
|
+
def test_double_prefix_caught(self) -> None:
|
|
262
|
+
"""Double prefix 'epic-epic-94' should definitely fail."""
|
|
263
|
+
shard = {
|
|
264
|
+
"id": "epic-epic-94",
|
|
265
|
+
"title": "Test",
|
|
266
|
+
"status": "backlog",
|
|
267
|
+
"stories": [],
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
result = validate_epic_shard(shard)
|
|
271
|
+
|
|
272
|
+
assert result.valid is False
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# =============================================================================
|
|
276
|
+
# AC2: _get_epic_ref() strips epic- prefix, prevents double-prefix filenames
|
|
277
|
+
# =============================================================================
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class TestGetEpicRefNormalization:
|
|
281
|
+
"""_get_epic_ref returns canonical references for shard filenames."""
|
|
282
|
+
|
|
283
|
+
def test_jira_key_preferred_over_id(self) -> None:
|
|
284
|
+
"""When both jira and id are present, Jira key wins."""
|
|
285
|
+
epic = {"id": "94", "jira": "MSSCI-14659"}
|
|
286
|
+
|
|
287
|
+
ref = _get_epic_ref(epic)
|
|
288
|
+
|
|
289
|
+
assert ref == "MSSCI-14659"
|
|
290
|
+
|
|
291
|
+
def test_strips_epic_prefix_from_id(self) -> None:
|
|
292
|
+
"""ID 'epic-94' should return '94' (prevents epic-epic-94.yaml)."""
|
|
293
|
+
epic = {"id": "epic-94"}
|
|
294
|
+
|
|
295
|
+
ref = _get_epic_ref(epic)
|
|
296
|
+
|
|
297
|
+
assert ref == "94"
|
|
298
|
+
|
|
299
|
+
def test_numeric_id_unchanged(self) -> None:
|
|
300
|
+
"""Plain numeric ID '94' should return '94'."""
|
|
301
|
+
epic = {"id": "94"}
|
|
302
|
+
|
|
303
|
+
ref = _get_epic_ref(epic)
|
|
304
|
+
|
|
305
|
+
assert ref == "94"
|
|
306
|
+
|
|
307
|
+
def test_jira_key_as_id_returned_directly(self) -> None:
|
|
308
|
+
"""ID that IS a Jira key should be returned directly."""
|
|
309
|
+
epic = {"id": "MSSCI-14510"}
|
|
310
|
+
|
|
311
|
+
ref = _get_epic_ref(epic)
|
|
312
|
+
|
|
313
|
+
assert ref == "MSSCI-14510"
|
|
314
|
+
|
|
315
|
+
def test_invalid_jira_key_falls_through(self) -> None:
|
|
316
|
+
"""Invalid Jira key in jira field should fall through to ID."""
|
|
317
|
+
epic = {"id": "94", "jira": "INVALID"}
|
|
318
|
+
|
|
319
|
+
ref = _get_epic_ref(epic)
|
|
320
|
+
|
|
321
|
+
# Invalid jira ignored, falls through to id
|
|
322
|
+
assert ref == "94"
|
|
323
|
+
|
|
324
|
+
def test_double_prefix_stripped(self) -> None:
|
|
325
|
+
"""'epic-epic-94' should strip all epic- prefixes to '94'."""
|
|
326
|
+
epic = {"id": "epic-epic-94"}
|
|
327
|
+
|
|
328
|
+
ref = _get_epic_ref(epic)
|
|
329
|
+
|
|
330
|
+
assert ref == "94"
|
|
331
|
+
|
|
332
|
+
def test_resulting_filename_is_correct(self) -> None:
|
|
333
|
+
"""Reference should produce correct filename: epic-{ref}.yaml."""
|
|
334
|
+
epic = {"id": "epic-94"}
|
|
335
|
+
ref = _get_epic_ref(epic)
|
|
336
|
+
|
|
337
|
+
filename = f"epic-{ref}.yaml"
|
|
338
|
+
|
|
339
|
+
assert filename == "epic-94.yaml"
|
|
340
|
+
assert "epic-epic-" not in filename
|
|
341
|
+
|
|
342
|
+
def test_jira_key_filename_correct(self) -> None:
|
|
343
|
+
"""Jira key ref should produce epic-MSSCI-14659.yaml."""
|
|
344
|
+
epic = {"id": "94", "jira": "MSSCI-14659"}
|
|
345
|
+
ref = _get_epic_ref(epic)
|
|
346
|
+
|
|
347
|
+
filename = f"epic-{ref}.yaml"
|
|
348
|
+
|
|
349
|
+
assert filename == "epic-MSSCI-14659.yaml"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# =============================================================================
|
|
353
|
+
# AC3: All four write paths call validator before write
|
|
354
|
+
# =============================================================================
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class TestWritePathValidatorIntegration:
|
|
358
|
+
"""All four epic creation paths call validate_epic_shard before writing."""
|
|
359
|
+
|
|
360
|
+
def test_epic_add_validates_before_write(self, tmp_path: Path) -> None:
|
|
361
|
+
"""epic_add.add_epic() should reject invalid epics before writing."""
|
|
362
|
+
from pennyfarthing_scripts.sprint.epic_add import add_epic
|
|
363
|
+
|
|
364
|
+
# Create a minimal sprint index
|
|
365
|
+
index = tmp_path / "current-sprint.yaml"
|
|
366
|
+
index.write_text("sprint:\n name: Test\nepics: []\n")
|
|
367
|
+
|
|
368
|
+
# Try to add an epic with prefix in ID (validator should catch)
|
|
369
|
+
result = add_epic(
|
|
370
|
+
sprint_path=index,
|
|
371
|
+
epic_id="epic-99",
|
|
372
|
+
title="Bad Epic",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
assert result["success"] is False
|
|
376
|
+
assert "validation" in result["error"].lower() or "epic-" in result["error"]
|
|
377
|
+
|
|
378
|
+
def test_epic_add_valid_epic_succeeds(self, tmp_path: Path) -> None:
|
|
379
|
+
"""epic_add.add_epic() should accept valid epics."""
|
|
380
|
+
from pennyfarthing_scripts.sprint.epic_add import add_epic
|
|
381
|
+
|
|
382
|
+
index = tmp_path / "current-sprint.yaml"
|
|
383
|
+
index.write_text("sprint:\n name: Test\nepics: []\n")
|
|
384
|
+
|
|
385
|
+
result = add_epic(
|
|
386
|
+
sprint_path=index,
|
|
387
|
+
epic_id="99",
|
|
388
|
+
title="Good Epic",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
assert result["success"] is True
|
|
392
|
+
|
|
393
|
+
def test_import_epic_validates_before_write(self, tmp_path: Path) -> None:
|
|
394
|
+
"""import_epic() validates generated YAML before writing to future.yaml."""
|
|
395
|
+
from pennyfarthing_scripts.sprint.import_epic import import_epic
|
|
396
|
+
|
|
397
|
+
# Create a markdown file with an epic
|
|
398
|
+
md_file = tmp_path / "epics.md"
|
|
399
|
+
md_file.write_text(
|
|
400
|
+
"# Test Initiative - Epics and Stories\n\n"
|
|
401
|
+
"## Overview\n\nTest description\n\n"
|
|
402
|
+
"## Epic 1: First Epic\n\n"
|
|
403
|
+
"**Points:** 5\n\n"
|
|
404
|
+
"### Story 1.1: First Story\n\n"
|
|
405
|
+
"**Points:** 5\n\n"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Create future.yaml
|
|
409
|
+
future = tmp_path / "sprint" / "future.yaml"
|
|
410
|
+
future.parent.mkdir(parents=True)
|
|
411
|
+
future.write_text(
|
|
412
|
+
"# Next Available Epic Number: 100\n"
|
|
413
|
+
"future:\n"
|
|
414
|
+
" initiatives: []\n"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
result = import_epic(
|
|
418
|
+
md_file,
|
|
419
|
+
initiative_name="Test Initiative",
|
|
420
|
+
project_root=tmp_path,
|
|
421
|
+
dry_run=True,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# If validation passes, dry_run should succeed
|
|
425
|
+
assert result["success"] is True
|
|
426
|
+
|
|
427
|
+
def test_epic_promote_validates_shard(self) -> None:
|
|
428
|
+
"""epic_promote calls validate_epic_shard before writing.
|
|
429
|
+
|
|
430
|
+
We verify by checking the source code imports and calls the validator.
|
|
431
|
+
Full integration test requires complex future.yaml setup.
|
|
432
|
+
"""
|
|
433
|
+
import inspect
|
|
434
|
+
|
|
435
|
+
from pennyfarthing_scripts.sprint import cli
|
|
436
|
+
|
|
437
|
+
# epic_promote is a Click Command; inspect the underlying callback
|
|
438
|
+
source = inspect.getsource(cli.epic_promote.callback)
|
|
439
|
+
assert "validate_epic_shard" in source
|
|
440
|
+
|
|
441
|
+
def test_jira_create_epic_validates_before_api_call(self) -> None:
|
|
442
|
+
"""create_epic_in_jira calls validate_epic_shard before Jira API.
|
|
443
|
+
|
|
444
|
+
Verified via source inspection since Jira API is external.
|
|
445
|
+
"""
|
|
446
|
+
import inspect
|
|
447
|
+
|
|
448
|
+
from pennyfarthing_scripts.jira.create import create_epic_in_jira
|
|
449
|
+
|
|
450
|
+
source = inspect.getsource(create_epic_in_jira)
|
|
451
|
+
assert "validate_epic_shard" in source
|
|
452
|
+
# Validator call should come before client.create_issue_sync
|
|
453
|
+
validator_pos = source.index("validate_epic_shard")
|
|
454
|
+
# The function should call validator before creating the issue
|
|
455
|
+
assert validator_pos > 0
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# =============================================================================
|
|
459
|
+
# AC4: _merge_epic_shards() emits warning for unresolvable refs
|
|
460
|
+
# =============================================================================
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class TestLoaderWarnings:
|
|
464
|
+
"""_merge_epic_shards emits warnings instead of silently skipping."""
|
|
465
|
+
|
|
466
|
+
def test_missing_shard_emits_warning(self, tmp_path: Path) -> None:
|
|
467
|
+
"""Unresolvable shard ref should emit a warning, not silently skip."""
|
|
468
|
+
from pennyfarthing_scripts.sprint.loader import _merge_epic_shards
|
|
469
|
+
|
|
470
|
+
data = {
|
|
471
|
+
"epics": ["MSSCI-99999"], # Doesn't exist
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
475
|
+
warnings.simplefilter("always")
|
|
476
|
+
_merge_epic_shards(data, tmp_path)
|
|
477
|
+
|
|
478
|
+
assert len(caught) == 1
|
|
479
|
+
assert "MSSCI-99999" in str(caught[0].message)
|
|
480
|
+
|
|
481
|
+
def test_missing_shard_excluded_from_result(self, tmp_path: Path) -> None:
|
|
482
|
+
"""Missing shard refs should not appear in the merged epics list."""
|
|
483
|
+
from pennyfarthing_scripts.sprint.loader import _merge_epic_shards
|
|
484
|
+
|
|
485
|
+
data = {"epics": ["MSSCI-99999"]}
|
|
486
|
+
|
|
487
|
+
with warnings.catch_warnings(record=True):
|
|
488
|
+
warnings.simplefilter("always")
|
|
489
|
+
result = _merge_epic_shards(data, tmp_path)
|
|
490
|
+
|
|
491
|
+
assert len(result["epics"]) == 0
|
|
492
|
+
|
|
493
|
+
def test_valid_shard_loaded_missing_shard_warned(self, tmp_path: Path) -> None:
|
|
494
|
+
"""Mix of valid and missing shards: load valid, warn on missing."""
|
|
495
|
+
from pennyfarthing_scripts.sprint.loader import _merge_epic_shards
|
|
496
|
+
|
|
497
|
+
# Create one valid shard
|
|
498
|
+
shard = tmp_path / "epic-MSSCI-14298.yaml"
|
|
499
|
+
shard.write_text(
|
|
500
|
+
"id: MSSCI-14298\ntitle: Valid Epic\nstatus: active\nstories: []\n"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
data = {"epics": ["MSSCI-14298", "MISSING-REF"]}
|
|
504
|
+
|
|
505
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
506
|
+
warnings.simplefilter("always")
|
|
507
|
+
result = _merge_epic_shards(data, tmp_path)
|
|
508
|
+
|
|
509
|
+
# One epic loaded, one warning
|
|
510
|
+
assert len(result["epics"]) == 1
|
|
511
|
+
assert result["epics"][0]["id"] == "MSSCI-14298"
|
|
512
|
+
assert len(caught) == 1
|
|
513
|
+
assert "MISSING-REF" in str(caught[0].message)
|
|
514
|
+
|
|
515
|
+
def test_non_sharded_data_unchanged(self, tmp_path: Path) -> None:
|
|
516
|
+
"""Non-sharded data (epics are dicts) should pass through unchanged."""
|
|
517
|
+
from pennyfarthing_scripts.sprint.loader import _merge_epic_shards
|
|
518
|
+
|
|
519
|
+
data = {
|
|
520
|
+
"epics": [
|
|
521
|
+
{"id": "94", "title": "Inline", "status": "backlog", "stories": []},
|
|
522
|
+
]
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
526
|
+
warnings.simplefilter("always")
|
|
527
|
+
result = _merge_epic_shards(data, tmp_path)
|
|
528
|
+
|
|
529
|
+
assert len(caught) == 0
|
|
530
|
+
assert result["epics"][0]["id"] == "94"
|
|
531
|
+
|
|
532
|
+
def test_strict_mode_promotes_warnings_to_errors(self, tmp_path: Path) -> None:
|
|
533
|
+
"""validate_sprint_file(strict=True) should treat missing refs as errors."""
|
|
534
|
+
sprint_file = tmp_path / "current-sprint.yaml"
|
|
535
|
+
sprint_file.write_text(
|
|
536
|
+
"sprint:\n"
|
|
537
|
+
" number: 12\n"
|
|
538
|
+
" jira_sprint_id: 276\n"
|
|
539
|
+
" goal: Test\n"
|
|
540
|
+
" start_date: 2026-01-20\n"
|
|
541
|
+
" end_date: 2026-02-02\n"
|
|
542
|
+
" status: active\n"
|
|
543
|
+
"epics:\n"
|
|
544
|
+
" - MISSING-REF\n"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
result = validate_sprint_file(sprint_file, strict=True)
|
|
548
|
+
|
|
549
|
+
assert result.valid is False
|
|
550
|
+
assert any("MISSING-REF" in e.message for e in result.errors)
|
|
551
|
+
|
|
552
|
+
def test_non_strict_mode_tolerates_missing_refs(self, tmp_path: Path) -> None:
|
|
553
|
+
"""validate_sprint_file(strict=False) should not fail on missing refs."""
|
|
554
|
+
sprint_file = tmp_path / "current-sprint.yaml"
|
|
555
|
+
sprint_file.write_text(
|
|
556
|
+
"sprint:\n"
|
|
557
|
+
" number: 12\n"
|
|
558
|
+
" jira_sprint_id: 276\n"
|
|
559
|
+
" goal: Test\n"
|
|
560
|
+
" start_date: 2026-01-20\n"
|
|
561
|
+
" end_date: 2026-02-02\n"
|
|
562
|
+
" status: active\n"
|
|
563
|
+
"epics:\n"
|
|
564
|
+
" - MISSING-REF\n"
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
result = validate_sprint_file(sprint_file, strict=False)
|
|
568
|
+
|
|
569
|
+
# Should be valid (warnings, not errors)
|
|
570
|
+
assert result.valid is True
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# =============================================================================
|
|
574
|
+
# AC5: Jira create epic checks for existing epic with same title
|
|
575
|
+
# =============================================================================
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class TestJiraIdempotencyGuard:
|
|
579
|
+
"""create_epic_in_jira checks for duplicate titles before creating."""
|
|
580
|
+
|
|
581
|
+
def test_idempotency_check_in_source(self) -> None:
|
|
582
|
+
"""Source code should contain duplicate title search logic."""
|
|
583
|
+
import inspect
|
|
584
|
+
|
|
585
|
+
from pennyfarthing_scripts.jira.create import create_epic_in_jira
|
|
586
|
+
|
|
587
|
+
source = inspect.getsource(create_epic_in_jira)
|
|
588
|
+
# Should search for existing epic with same title
|
|
589
|
+
assert "search_issues_sync" in source or "duplicate" in source.lower()
|
|
590
|
+
|
|
591
|
+
def test_force_flag_parameter_exists(self) -> None:
|
|
592
|
+
"""create_epic_in_jira should accept a force parameter."""
|
|
593
|
+
import inspect
|
|
594
|
+
|
|
595
|
+
from pennyfarthing_scripts.jira.create import create_epic_in_jira
|
|
596
|
+
|
|
597
|
+
sig = inspect.signature(create_epic_in_jira)
|
|
598
|
+
assert "force" in sig.parameters
|
|
599
|
+
|
|
600
|
+
def test_force_default_is_false(self) -> None:
|
|
601
|
+
"""force parameter should default to False (safe by default)."""
|
|
602
|
+
import inspect
|
|
603
|
+
|
|
604
|
+
from pennyfarthing_scripts.jira.create import create_epic_in_jira
|
|
605
|
+
|
|
606
|
+
sig = inspect.signature(create_epic_in_jira)
|
|
607
|
+
assert sig.parameters["force"].default is False
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# =============================================================================
|
|
611
|
+
# AC6: Integration — existing validator tests still pass, new rules covered
|
|
612
|
+
# =============================================================================
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class TestValidationIntegration:
|
|
616
|
+
"""Integration tests ensuring all validation rules work together."""
|
|
617
|
+
|
|
618
|
+
def test_full_valid_shard_roundtrip(
|
|
619
|
+
self, tmp_path: Path, valid_epic_shard_with_jira: dict[str, Any]
|
|
620
|
+
) -> None:
|
|
621
|
+
"""Valid shard passes validation, writes correctly, reads back."""
|
|
622
|
+
from pennyfarthing_scripts.sprint.yaml_io import _get_epic_ref
|
|
623
|
+
|
|
624
|
+
# Validate
|
|
625
|
+
result = validate_epic_shard(valid_epic_shard_with_jira)
|
|
626
|
+
assert result.valid is True
|
|
627
|
+
|
|
628
|
+
# Get ref
|
|
629
|
+
ref = _get_epic_ref(valid_epic_shard_with_jira)
|
|
630
|
+
assert ref == "MSSCI-14659"
|
|
631
|
+
|
|
632
|
+
# Write shard file
|
|
633
|
+
shard_path = tmp_path / f"epic-{ref}.yaml"
|
|
634
|
+
yaml_content = yaml.dump(
|
|
635
|
+
valid_epic_shard_with_jira, default_flow_style=False
|
|
636
|
+
)
|
|
637
|
+
shard_path.write_text(yaml_content)
|
|
638
|
+
|
|
639
|
+
# Verify file name is correct
|
|
640
|
+
assert shard_path.name == "epic-MSSCI-14659.yaml"
|
|
641
|
+
assert "epic-epic-" not in shard_path.name
|
|
642
|
+
|
|
643
|
+
def test_bad_shard_blocked_at_all_gates(self) -> None:
|
|
644
|
+
"""An epic with epic- prefix should be caught by validator."""
|
|
645
|
+
bad_shard = {
|
|
646
|
+
"id": "epic-94",
|
|
647
|
+
"title": "Bad Epic",
|
|
648
|
+
"status": "backlog",
|
|
649
|
+
"stories": [],
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
result = validate_epic_shard(bad_shard)
|
|
653
|
+
assert result.valid is False
|
|
654
|
+
|
|
655
|
+
def test_validation_result_accumulates_multiple_errors(self) -> None:
|
|
656
|
+
"""Multiple validation failures should all be reported."""
|
|
657
|
+
terrible_shard = {
|
|
658
|
+
"id": "epic-bad",
|
|
659
|
+
"jira": "INVALID",
|
|
660
|
+
"stories": "not a list",
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
result = validate_epic_shard(terrible_shard)
|
|
664
|
+
|
|
665
|
+
assert result.valid is False
|
|
666
|
+
# Should report: missing title, missing status, epic- prefix,
|
|
667
|
+
# invalid jira, stories not a list
|
|
668
|
+
assert len(result.errors) >= 3
|
|
669
|
+
|
|
670
|
+
def test_real_sprint_validates(self) -> None:
|
|
671
|
+
"""The actual current-sprint.yaml should pass validation."""
|
|
672
|
+
project_root = Path(__file__).parent.parent.parent
|
|
673
|
+
sprint_file = project_root / "sprint" / "current-sprint.yaml"
|
|
674
|
+
|
|
675
|
+
if not sprint_file.exists():
|
|
676
|
+
pytest.skip("No current-sprint.yaml available")
|
|
677
|
+
|
|
678
|
+
result = validate_sprint_file(sprint_file)
|
|
679
|
+
|
|
680
|
+
assert isinstance(result, ValidationResult)
|
|
681
|
+
if not result.valid:
|
|
682
|
+
for error in result.errors:
|
|
683
|
+
print(f" {error.path}: {error.message}")
|
|
684
|
+
|
|
685
|
+
def test_real_sprint_strict_mode(self) -> None:
|
|
686
|
+
"""The actual current-sprint.yaml should pass strict validation."""
|
|
687
|
+
project_root = Path(__file__).parent.parent.parent
|
|
688
|
+
sprint_file = project_root / "sprint" / "current-sprint.yaml"
|
|
689
|
+
|
|
690
|
+
if not sprint_file.exists():
|
|
691
|
+
pytest.skip("No current-sprint.yaml available")
|
|
692
|
+
|
|
693
|
+
result = validate_sprint_file(sprint_file, strict=True)
|
|
694
|
+
|
|
695
|
+
assert isinstance(result, ValidationResult)
|
|
696
|
+
# In strict mode, all shard refs should be resolvable
|
|
697
|
+
if not result.valid:
|
|
698
|
+
for error in result.errors:
|
|
699
|
+
print(f" {error.path}: {error.message}")
|