@pennyfarthing/core 10.0.2 → 10.0.5

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.
Files changed (722) hide show
  1. package/README.md +287 -0
  2. package/package.json +29 -41
  3. package/{dist → packages/core/dist}/cli/commands/cyclist.d.ts +5 -1
  4. package/packages/core/dist/cli/commands/cyclist.d.ts.map +1 -0
  5. package/{dist → packages/core/dist}/cli/commands/cyclist.js +4 -4
  6. package/packages/core/dist/cli/commands/cyclist.js.map +1 -0
  7. package/{dist → packages/core/dist}/cli/commands/cyclist.test.js +2 -2
  8. package/packages/core/dist/cli/commands/cyclist.test.js.map +1 -0
  9. package/packages/core/dist/cli/commands/doctor-file-layout.test.d.ts +13 -0
  10. package/packages/core/dist/cli/commands/doctor-file-layout.test.d.ts.map +1 -0
  11. package/packages/core/dist/cli/commands/doctor-file-layout.test.js +234 -0
  12. package/packages/core/dist/cli/commands/doctor-file-layout.test.js.map +1 -0
  13. package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.js +17 -16
  14. package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.js.map +1 -1
  15. package/{dist → packages/core/dist}/cli/commands/doctor.d.ts +8 -0
  16. package/{dist → packages/core/dist}/cli/commands/doctor.d.ts.map +1 -1
  17. package/{dist → packages/core/dist}/cli/commands/doctor.js +224 -3
  18. package/packages/core/dist/cli/commands/doctor.js.map +1 -0
  19. package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.js +1 -1
  20. package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.js.map +1 -1
  21. package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.js +1 -1
  22. package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.js.map +1 -1
  23. package/packages/core/dist/cli/commands/hooks-consolidation.test.d.ts +19 -0
  24. package/packages/core/dist/cli/commands/hooks-consolidation.test.d.ts.map +1 -0
  25. package/packages/core/dist/cli/commands/hooks-consolidation.test.js +358 -0
  26. package/packages/core/dist/cli/commands/hooks-consolidation.test.js.map +1 -0
  27. package/{dist → packages/core/dist}/cli/commands/init.d.ts.map +1 -1
  28. package/{dist → packages/core/dist}/cli/commands/init.js +3 -0
  29. package/packages/core/dist/cli/commands/init.js.map +1 -0
  30. package/{dist → packages/core/dist}/cli/commands/update.d.ts.map +1 -1
  31. package/{dist → packages/core/dist}/cli/commands/update.js +53 -1
  32. package/{dist → packages/core/dist}/cli/commands/update.js.map +1 -1
  33. package/{dist → packages/core/dist}/cli/ocean-profiles.test.js +1 -1
  34. package/{dist → packages/core/dist}/cli/ocean-profiles.test.js.map +1 -1
  35. package/{dist → packages/core/dist}/cli/utils/files.d.ts +10 -0
  36. package/{dist → packages/core/dist}/cli/utils/files.d.ts.map +1 -1
  37. package/{dist → packages/core/dist}/cli/utils/files.js +35 -0
  38. package/{dist → packages/core/dist}/cli/utils/files.js.map +1 -1
  39. package/{dist → packages/core/dist}/cli/utils/settings.d.ts.map +1 -1
  40. package/{dist → packages/core/dist}/cli/utils/settings.js +24 -0
  41. package/packages/core/dist/cli/utils/settings.js.map +1 -0
  42. package/{dist → packages/core/dist}/cli/utils/themes.d.ts +1 -0
  43. package/packages/core/dist/cli/utils/themes.d.ts.map +1 -0
  44. package/{dist → packages/core/dist}/cli/utils/themes.js.map +1 -1
  45. package/{dist → packages/core/dist}/scripts/generate-report.d.ts.map +1 -1
  46. package/{dist → packages/core/dist}/scripts/generate-report.js +11 -7
  47. package/packages/core/dist/scripts/generate-report.js.map +1 -0
  48. package/{dist → packages/core/dist}/scripts/generate-spider-report.d.ts.map +1 -1
  49. package/{dist → packages/core/dist}/scripts/generate-spider-report.js +12 -8
  50. package/packages/core/dist/scripts/generate-spider-report.js.map +1 -0
  51. package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -0
  52. package/{dist → packages/core/dist}/scripts/generate-spider.js +6 -4
  53. package/packages/core/dist/scripts/generate-spider.js.map +1 -0
  54. package/{dist → packages/core/dist}/scripts/generate-spider.test.js +2 -2
  55. package/packages/core/dist/scripts/generate-spider.test.js.map +1 -0
  56. package/pennyfarthing-dist/agents/pm.md +1 -1
  57. package/pennyfarthing-dist/agents/sm-finish.md +1 -1
  58. package/pennyfarthing-dist/agents/sm-setup.md +6 -6
  59. package/pennyfarthing-dist/agents/sm.md +12 -6
  60. package/pennyfarthing-dist/agents/workflow-status-check.md +1 -1
  61. package/pennyfarthing-dist/commands/repo-status.md +2 -2
  62. package/pennyfarthing-dist/commands/sprint.md +8 -8
  63. package/pennyfarthing-dist/guides/bell-mode.md +65 -0
  64. package/pennyfarthing-dist/guides/benchmarks.md +62 -0
  65. package/pennyfarthing-dist/guides/bikelane.md +86 -0
  66. package/pennyfarthing-dist/guides/prime.md +72 -0
  67. package/pennyfarthing-dist/guides/reflector.md +59 -0
  68. package/pennyfarthing-dist/guides/relay-mode.md +53 -0
  69. package/pennyfarthing-dist/guides/skill-schema.md +25 -26
  70. package/pennyfarthing-dist/guides/tirepump.md +54 -0
  71. package/pennyfarthing-dist/guides/xml-tags.md +2 -2
  72. package/pennyfarthing-dist/personas/themes/battlestar-galactica.yaml +59 -58
  73. package/pennyfarthing-dist/personas/themes/blade-runner.yaml +10 -10
  74. package/pennyfarthing-dist/personas/themes/doctor-who.yaml +10 -10
  75. package/pennyfarthing-dist/personas/themes/dune.yaml +64 -69
  76. package/pennyfarthing-dist/personas/themes/firefly.yaml +60 -73
  77. package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +60 -69
  78. package/pennyfarthing-dist/personas/themes/harry-potter.yaml +59 -73
  79. package/pennyfarthing-dist/personas/themes/hitchhikers-guide.yaml +45 -57
  80. package/pennyfarthing-dist/personas/themes/mad-max.yaml +5 -11
  81. package/pennyfarthing-dist/personas/themes/princess-bride.yaml +53 -63
  82. package/pennyfarthing-dist/personas/themes/sandman.yaml +59 -59
  83. package/pennyfarthing-dist/personas/themes/the-matrix.yaml +61 -62
  84. package/pennyfarthing-dist/personas/themes/west-wing.yaml +8 -9
  85. package/pennyfarthing-dist/scripts/README.md +2 -2
  86. package/pennyfarthing-dist/scripts/git/git-status-all.sh +1 -1
  87. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +3 -3
  88. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +32 -0
  89. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -7
  90. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +1 -1
  91. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +12 -91
  92. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +11 -86
  93. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +11 -255
  94. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +3 -3
  95. package/pennyfarthing-dist/scripts/sprint/README.md +32 -17
  96. package/pennyfarthing-dist/scripts/story/README.md +1 -1
  97. package/pennyfarthing-dist/scripts/test/test-setup.sh +1 -1
  98. package/pennyfarthing-dist/skills/jira/SKILL.md +107 -408
  99. package/pennyfarthing-dist/skills/skill-registry.yaml +21 -12
  100. package/pennyfarthing-dist/skills/sprint/skill.md +386 -68
  101. package/pennyfarthing-dist/skills/story/skill.md +14 -206
  102. package/pennyfarthing-dist/templates/settings.local.json.template +9 -1
  103. package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +1 -1
  104. package/pennyfarthing-dist/workflows/git-cleanup.yaml +1 -1
  105. package/pennyfarthing-dist/workflows/project-setup/steps/step-10-complete.md +1 -1
  106. package/pennyfarthing_scripts/README.md +66 -0
  107. package/pennyfarthing_scripts/__init__.py +17 -0
  108. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  109. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  110. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  111. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  112. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  113. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  114. package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
  115. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  116. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  117. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
  119. package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
  120. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  121. package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
  122. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  123. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  124. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  125. package/pennyfarthing_scripts/bellmode_hook.py +154 -0
  126. package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
  127. package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
  128. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  130. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/brownfield/cli.py +131 -0
  133. package/pennyfarthing_scripts/brownfield/discover.py +753 -0
  134. package/pennyfarthing_scripts/cli.py +184 -0
  135. package/pennyfarthing_scripts/common/__init__.py +49 -0
  136. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  137. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  138. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/common/config.py +92 -0
  141. package/pennyfarthing_scripts/common/output.py +180 -0
  142. package/pennyfarthing_scripts/common/themes.py +253 -0
  143. package/pennyfarthing_scripts/config.py +21 -0
  144. package/pennyfarthing_scripts/context.py +414 -0
  145. package/pennyfarthing_scripts/git/__init__.py +29 -0
  146. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  147. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  148. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  149. package/pennyfarthing_scripts/git/create_branches.py +439 -0
  150. package/pennyfarthing_scripts/git/status_all.py +310 -0
  151. package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +7 -0
  152. package/pennyfarthing_scripts/hooks.py +454 -0
  153. package/pennyfarthing_scripts/hotspots/__init__.py +31 -0
  154. package/pennyfarthing_scripts/hotspots/__main__.py +6 -0
  155. package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  156. package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
  157. package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  158. package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  159. package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
  160. package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/hotspots/analyze.py +472 -0
  162. package/pennyfarthing_scripts/hotspots/cli.py +152 -0
  163. package/pennyfarthing_scripts/hotspots/formatters.py +109 -0
  164. package/pennyfarthing_scripts/hotspots/models.py +60 -0
  165. package/pennyfarthing_scripts/jira/__init__.py +99 -0
  166. package/pennyfarthing_scripts/jira/__main__.py +10 -0
  167. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  171. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  172. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
  183. package/pennyfarthing_scripts/jira/claim.py +211 -0
  184. package/pennyfarthing_scripts/jira/cli.py +351 -0
  185. package/pennyfarthing_scripts/jira/client.py +762 -0
  186. package/pennyfarthing_scripts/jira/create.py +267 -0
  187. package/pennyfarthing_scripts/jira/epic.py +176 -0
  188. package/pennyfarthing_scripts/jira/operations.py +124 -0
  189. package/pennyfarthing_scripts/jira/reconcile.py +277 -0
  190. package/pennyfarthing_scripts/jira/story.py +219 -0
  191. package/pennyfarthing_scripts/jira/sync.py +350 -0
  192. package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
  193. package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
  194. package/pennyfarthing_scripts/jira_sync.py +36 -0
  195. package/pennyfarthing_scripts/jira_sync_story.py +30 -0
  196. package/pennyfarthing_scripts/migration/__init__.py +39 -0
  197. package/pennyfarthing_scripts/migration/__main__.py +10 -0
  198. package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
  201. package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
  202. package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
  203. package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/migration/cli.py +304 -0
  206. package/pennyfarthing_scripts/migration/session.py +384 -0
  207. package/pennyfarthing_scripts/migration/skill.py +188 -0
  208. package/pennyfarthing_scripts/migration/step.py +229 -0
  209. package/pennyfarthing_scripts/migration/validate.py +282 -0
  210. package/pennyfarthing_scripts/output.py +37 -0
  211. package/pennyfarthing_scripts/patch_mode.py +449 -0
  212. package/pennyfarthing_scripts/preflight/__init__.py +17 -0
  213. package/pennyfarthing_scripts/preflight/__main__.py +10 -0
  214. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  215. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  216. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  217. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/preflight/cli.py +141 -0
  219. package/pennyfarthing_scripts/preflight/finish.py +382 -0
  220. package/pennyfarthing_scripts/pretooluse_hook.py +193 -0
  221. package/pennyfarthing_scripts/prime/__init__.py +125 -0
  222. package/pennyfarthing_scripts/prime/__main__.py +8 -0
  223. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  225. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  226. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  227. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  228. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  229. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  232. package/pennyfarthing_scripts/prime/cli.py +645 -0
  233. package/pennyfarthing_scripts/prime/loader.py +239 -0
  234. package/pennyfarthing_scripts/prime/models.py +206 -0
  235. package/pennyfarthing_scripts/prime/persona.py +309 -0
  236. package/pennyfarthing_scripts/prime/session.py +183 -0
  237. package/pennyfarthing_scripts/prime/tiers.py +201 -0
  238. package/pennyfarthing_scripts/prime/workflow.py +277 -0
  239. package/pennyfarthing_scripts/schema_validation_hook.py +306 -0
  240. package/pennyfarthing_scripts/sprint/__init__.py +66 -0
  241. package/pennyfarthing_scripts/sprint/__main__.py +10 -0
  242. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  243. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  244. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  245. package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  247. package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  248. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  249. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  255. package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  256. package/pennyfarthing_scripts/sprint/archive.py +165 -0
  257. package/pennyfarthing_scripts/sprint/archive_epic.py +408 -0
  258. package/pennyfarthing_scripts/sprint/cli.py +1863 -0
  259. package/pennyfarthing_scripts/sprint/epic_add.py +173 -0
  260. package/pennyfarthing_scripts/sprint/import_epic.py +431 -0
  261. package/pennyfarthing_scripts/sprint/loader.py +237 -0
  262. package/pennyfarthing_scripts/sprint/status.py +122 -0
  263. package/pennyfarthing_scripts/sprint/story_add.py +187 -0
  264. package/pennyfarthing_scripts/sprint/story_update.py +181 -0
  265. package/pennyfarthing_scripts/sprint/validate_cmd.py +307 -0
  266. package/pennyfarthing_scripts/sprint/validator.py +580 -0
  267. package/pennyfarthing_scripts/sprint/work.py +208 -0
  268. package/pennyfarthing_scripts/sprint/yaml_io.py +367 -0
  269. package/pennyfarthing_scripts/story/__init__.py +67 -0
  270. package/pennyfarthing_scripts/story/__main__.py +10 -0
  271. package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
  272. package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
  273. package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
  274. package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
  275. package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
  276. package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
  277. package/pennyfarthing_scripts/story/cli.py +105 -0
  278. package/pennyfarthing_scripts/story/create.py +167 -0
  279. package/pennyfarthing_scripts/story/size.py +113 -0
  280. package/pennyfarthing_scripts/story/template.py +151 -0
  281. package/pennyfarthing_scripts/swebench.py +216 -0
  282. package/pennyfarthing_scripts/tests/__init__.py +1 -0
  283. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  285. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  286. package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
  287. package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
  288. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  289. package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
  290. package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
  291. package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
  292. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  293. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
  294. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  295. package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
  296. package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
  297. package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
  298. package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
  299. package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
  300. package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
  301. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  302. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  303. package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
  304. package/pennyfarthing_scripts/tests/conftest.py +106 -0
  305. package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
  306. package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
  307. package/pennyfarthing_scripts/tests/test_common.py +180 -0
  308. package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
  309. package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
  310. package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
  311. package/pennyfarthing_scripts/tests/test_patch_mode.py +830 -0
  312. package/pennyfarthing_scripts/tests/test_prime.py +1050 -0
  313. package/pennyfarthing_scripts/tests/test_sprint_package.py +402 -0
  314. package/pennyfarthing_scripts/tests/test_sprint_validator.py +731 -0
  315. package/pennyfarthing_scripts/tests/test_story_add.py +921 -0
  316. package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
  317. package/pennyfarthing_scripts/tests/test_story_update.py +769 -0
  318. package/pennyfarthing_scripts/tests/test_tiers.py +1090 -0
  319. package/pennyfarthing_scripts/tests/test_token_counting.py +559 -0
  320. package/pennyfarthing_scripts/tests/test_validate_cmd.py +500 -0
  321. package/pennyfarthing_scripts/tests/test_workflow_check.py +341 -0
  322. package/pennyfarthing_scripts/tests/test_yaml_io.py +815 -0
  323. package/pennyfarthing_scripts/welcome_hook.py +157 -0
  324. package/pennyfarthing_scripts/workflow.py +287 -0
  325. package/scripts/postinstall.cjs +34 -0
  326. package/dist/cli/commands/cyclist.d.ts.map +0 -1
  327. package/dist/cli/commands/cyclist.js.map +0 -1
  328. package/dist/cli/commands/cyclist.test.js.map +0 -1
  329. package/dist/cli/commands/doctor.js.map +0 -1
  330. package/dist/cli/commands/init.js.map +0 -1
  331. package/dist/cli/utils/settings.js.map +0 -1
  332. package/dist/cli/utils/themes.d.ts.map +0 -1
  333. package/dist/scripts/generate-report.js.map +0 -1
  334. package/dist/scripts/generate-spider-report.js.map +0 -1
  335. package/dist/scripts/generate-spider.d.ts.map +0 -1
  336. package/dist/scripts/generate-spider.js.map +0 -1
  337. package/dist/scripts/generate-spider.test.js.map +0 -1
  338. package/pennyfarthing-dist/scripts/jira/jira-lib.sh +0 -464
  339. package/pennyfarthing-dist/scripts/jira/jira-sync.sh +0 -16
  340. package/pennyfarthing-dist/scripts/jira/sync-epic-to-jira.sh +0 -16
  341. package/pennyfarthing-dist/scripts/sprint/archive-story.sh +0 -133
  342. package/pennyfarthing-dist/scripts/sprint/available-stories.sh +0 -91
  343. package/pennyfarthing-dist/scripts/sprint/check-story.sh +0 -158
  344. package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +0 -52
  345. package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +0 -63
  346. package/pennyfarthing-dist/scripts/sprint/list-future.sh +0 -145
  347. package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +0 -110
  348. package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +0 -148
  349. package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +0 -415
  350. package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +0 -33
  351. package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +0 -230
  352. package/pennyfarthing-dist/scripts/sprint/sprint-status.sh +0 -134
  353. package/pennyfarthing-dist/scripts/sprint/validate-sprint-yaml.sh +0 -139
  354. package/pennyfarthing-dist/skills/sprint/scripts/archive-story.sh +0 -101
  355. package/pennyfarthing-dist/skills/sprint/scripts/available-stories.sh +0 -97
  356. package/pennyfarthing-dist/skills/sprint/scripts/check-story.sh +0 -164
  357. package/pennyfarthing-dist/skills/sprint/scripts/create-jira-epic.sh +0 -101
  358. package/pennyfarthing-dist/skills/sprint/scripts/new-sprint.sh +0 -116
  359. package/pennyfarthing-dist/skills/sprint/scripts/promote-epic.sh +0 -164
  360. package/pennyfarthing-dist/skills/sprint/scripts/sprint-info.sh +0 -39
  361. package/pennyfarthing-dist/skills/sprint/scripts/sprint-status.sh +0 -147
  362. package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +0 -93
  363. /package/{bin → packages/core/bin}/pennyfarthing.js +0 -0
  364. /package/{dist → packages/core/dist}/bmad/context-reader.d.ts +0 -0
  365. /package/{dist → packages/core/dist}/bmad/context-reader.d.ts.map +0 -0
  366. /package/{dist → packages/core/dist}/bmad/context-reader.js +0 -0
  367. /package/{dist → packages/core/dist}/bmad/context-reader.js.map +0 -0
  368. /package/{dist → packages/core/dist}/bmad/context-reader.test.d.ts +0 -0
  369. /package/{dist → packages/core/dist}/bmad/context-reader.test.d.ts.map +0 -0
  370. /package/{dist → packages/core/dist}/bmad/context-reader.test.js +0 -0
  371. /package/{dist → packages/core/dist}/bmad/context-reader.test.js.map +0 -0
  372. /package/{dist → packages/core/dist}/bmad/epics-parser.d.ts +0 -0
  373. /package/{dist → packages/core/dist}/bmad/epics-parser.d.ts.map +0 -0
  374. /package/{dist → packages/core/dist}/bmad/epics-parser.js +0 -0
  375. /package/{dist → packages/core/dist}/bmad/epics-parser.js.map +0 -0
  376. /package/{dist → packages/core/dist}/bmad/epics-parser.test.d.ts +0 -0
  377. /package/{dist → packages/core/dist}/bmad/epics-parser.test.d.ts.map +0 -0
  378. /package/{dist → packages/core/dist}/bmad/epics-parser.test.js +0 -0
  379. /package/{dist → packages/core/dist}/bmad/epics-parser.test.js.map +0 -0
  380. /package/{dist → packages/core/dist}/bmad/index.d.ts +0 -0
  381. /package/{dist → packages/core/dist}/bmad/index.d.ts.map +0 -0
  382. /package/{dist → packages/core/dist}/bmad/index.js +0 -0
  383. /package/{dist → packages/core/dist}/bmad/index.js.map +0 -0
  384. /package/{dist → packages/core/dist}/bmad/status-sync.d.ts +0 -0
  385. /package/{dist → packages/core/dist}/bmad/status-sync.d.ts.map +0 -0
  386. /package/{dist → packages/core/dist}/bmad/status-sync.js +0 -0
  387. /package/{dist → packages/core/dist}/bmad/status-sync.js.map +0 -0
  388. /package/{dist → packages/core/dist}/bmad/status-sync.test.d.ts +0 -0
  389. /package/{dist → packages/core/dist}/bmad/status-sync.test.d.ts.map +0 -0
  390. /package/{dist → packages/core/dist}/bmad/status-sync.test.js +0 -0
  391. /package/{dist → packages/core/dist}/bmad/status-sync.test.js.map +0 -0
  392. /package/{dist → packages/core/dist}/bmad/story-exporter.d.ts +0 -0
  393. /package/{dist → packages/core/dist}/bmad/story-exporter.d.ts.map +0 -0
  394. /package/{dist → packages/core/dist}/bmad/story-exporter.js +0 -0
  395. /package/{dist → packages/core/dist}/bmad/story-exporter.js.map +0 -0
  396. /package/{dist → packages/core/dist}/bmad/story-exporter.test.d.ts +0 -0
  397. /package/{dist → packages/core/dist}/bmad/story-exporter.test.d.ts.map +0 -0
  398. /package/{dist → packages/core/dist}/bmad/story-exporter.test.js +0 -0
  399. /package/{dist → packages/core/dist}/bmad/story-exporter.test.js.map +0 -0
  400. /package/{dist → packages/core/dist}/bmad/story-parser.d.ts +0 -0
  401. /package/{dist → packages/core/dist}/bmad/story-parser.d.ts.map +0 -0
  402. /package/{dist → packages/core/dist}/bmad/story-parser.js +0 -0
  403. /package/{dist → packages/core/dist}/bmad/story-parser.js.map +0 -0
  404. /package/{dist → packages/core/dist}/bmad/story-parser.test.d.ts +0 -0
  405. /package/{dist → packages/core/dist}/bmad/story-parser.test.d.ts.map +0 -0
  406. /package/{dist → packages/core/dist}/bmad/story-parser.test.js +0 -0
  407. /package/{dist → packages/core/dist}/bmad/story-parser.test.js.map +0 -0
  408. /package/{dist → packages/core/dist}/cli/commands/command.d.ts +0 -0
  409. /package/{dist → packages/core/dist}/cli/commands/command.d.ts.map +0 -0
  410. /package/{dist → packages/core/dist}/cli/commands/command.js +0 -0
  411. /package/{dist → packages/core/dist}/cli/commands/command.js.map +0 -0
  412. /package/{dist → packages/core/dist}/cli/commands/cyclist.test.d.ts +0 -0
  413. /package/{dist → packages/core/dist}/cli/commands/cyclist.test.d.ts.map +0 -0
  414. /package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.d.ts +0 -0
  415. /package/{dist → packages/core/dist}/cli/commands/doctor-legacy.test.d.ts.map +0 -0
  416. /package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.d.ts +0 -0
  417. /package/{dist → packages/core/dist}/cli/commands/e2e-fresh-install.test.d.ts.map +0 -0
  418. /package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.d.ts +0 -0
  419. /package/{dist → packages/core/dist}/cli/commands/e2e-upgrade.test.d.ts.map +0 -0
  420. /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.d.ts +0 -0
  421. /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.d.ts.map +0 -0
  422. /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.js +0 -0
  423. /package/{dist → packages/core/dist}/cli/commands/init-consolidation.test.js.map +0 -0
  424. /package/{dist → packages/core/dist}/cli/commands/init.d.ts +0 -0
  425. /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.d.ts +0 -0
  426. /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.d.ts.map +0 -0
  427. /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.js +0 -0
  428. /package/{dist → packages/core/dist}/cli/commands/persona-config-consolidation.test.js.map +0 -0
  429. /package/{dist → packages/core/dist}/cli/commands/skill.d.ts +0 -0
  430. /package/{dist → packages/core/dist}/cli/commands/skill.d.ts.map +0 -0
  431. /package/{dist → packages/core/dist}/cli/commands/skill.js +0 -0
  432. /package/{dist → packages/core/dist}/cli/commands/skill.js.map +0 -0
  433. /package/{dist → packages/core/dist}/cli/commands/theme.d.ts +0 -0
  434. /package/{dist → packages/core/dist}/cli/commands/theme.d.ts.map +0 -0
  435. /package/{dist → packages/core/dist}/cli/commands/theme.js +0 -0
  436. /package/{dist → packages/core/dist}/cli/commands/theme.js.map +0 -0
  437. /package/{dist → packages/core/dist}/cli/commands/uninstall.d.ts +0 -0
  438. /package/{dist → packages/core/dist}/cli/commands/uninstall.d.ts.map +0 -0
  439. /package/{dist → packages/core/dist}/cli/commands/uninstall.js +0 -0
  440. /package/{dist → packages/core/dist}/cli/commands/uninstall.js.map +0 -0
  441. /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.d.ts +0 -0
  442. /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.d.ts.map +0 -0
  443. /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.js +0 -0
  444. /package/{dist → packages/core/dist}/cli/commands/update-consolidation.test.js.map +0 -0
  445. /package/{dist → packages/core/dist}/cli/commands/update.d.ts +0 -0
  446. /package/{dist → packages/core/dist}/cli/commands/version.d.ts +0 -0
  447. /package/{dist → packages/core/dist}/cli/commands/version.d.ts.map +0 -0
  448. /package/{dist → packages/core/dist}/cli/commands/version.js +0 -0
  449. /package/{dist → packages/core/dist}/cli/commands/version.js.map +0 -0
  450. /package/{dist → packages/core/dist}/cli/customization.test.d.ts +0 -0
  451. /package/{dist → packages/core/dist}/cli/customization.test.d.ts.map +0 -0
  452. /package/{dist → packages/core/dist}/cli/customization.test.js +0 -0
  453. /package/{dist → packages/core/dist}/cli/customization.test.js.map +0 -0
  454. /package/{dist → packages/core/dist}/cli/cyclist-migration.test.d.ts +0 -0
  455. /package/{dist → packages/core/dist}/cli/cyclist-migration.test.d.ts.map +0 -0
  456. /package/{dist → packages/core/dist}/cli/cyclist-migration.test.js +0 -0
  457. /package/{dist → packages/core/dist}/cli/cyclist-migration.test.js.map +0 -0
  458. /package/{dist → packages/core/dist}/cli/index.d.ts +0 -0
  459. /package/{dist → packages/core/dist}/cli/index.d.ts.map +0 -0
  460. /package/{dist → packages/core/dist}/cli/index.js +0 -0
  461. /package/{dist → packages/core/dist}/cli/index.js.map +0 -0
  462. /package/{dist → packages/core/dist}/cli/ocean-profiles.test.d.ts +0 -0
  463. /package/{dist → packages/core/dist}/cli/ocean-profiles.test.d.ts.map +0 -0
  464. /package/{dist → packages/core/dist}/cli/theme-maker.test.d.ts +0 -0
  465. /package/{dist → packages/core/dist}/cli/theme-maker.test.d.ts.map +0 -0
  466. /package/{dist → packages/core/dist}/cli/theme-maker.test.js +0 -0
  467. /package/{dist → packages/core/dist}/cli/theme-maker.test.js.map +0 -0
  468. /package/{dist → packages/core/dist}/cli/utils/constants.d.ts +0 -0
  469. /package/{dist → packages/core/dist}/cli/utils/constants.d.ts.map +0 -0
  470. /package/{dist → packages/core/dist}/cli/utils/constants.js +0 -0
  471. /package/{dist → packages/core/dist}/cli/utils/constants.js.map +0 -0
  472. /package/{dist → packages/core/dist}/cli/utils/logger.d.ts +0 -0
  473. /package/{dist → packages/core/dist}/cli/utils/logger.d.ts.map +0 -0
  474. /package/{dist → packages/core/dist}/cli/utils/logger.js +0 -0
  475. /package/{dist → packages/core/dist}/cli/utils/logger.js.map +0 -0
  476. /package/{dist → packages/core/dist}/cli/utils/manifest.d.ts +0 -0
  477. /package/{dist → packages/core/dist}/cli/utils/manifest.d.ts.map +0 -0
  478. /package/{dist → packages/core/dist}/cli/utils/manifest.js +0 -0
  479. /package/{dist → packages/core/dist}/cli/utils/manifest.js.map +0 -0
  480. /package/{dist → packages/core/dist}/cli/utils/node-modules.d.ts +0 -0
  481. /package/{dist → packages/core/dist}/cli/utils/node-modules.d.ts.map +0 -0
  482. /package/{dist → packages/core/dist}/cli/utils/node-modules.js +0 -0
  483. /package/{dist → packages/core/dist}/cli/utils/node-modules.js.map +0 -0
  484. /package/{dist → packages/core/dist}/cli/utils/prompts.d.ts +0 -0
  485. /package/{dist → packages/core/dist}/cli/utils/prompts.d.ts.map +0 -0
  486. /package/{dist → packages/core/dist}/cli/utils/prompts.js +0 -0
  487. /package/{dist → packages/core/dist}/cli/utils/prompts.js.map +0 -0
  488. /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.d.ts +0 -0
  489. /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.d.ts.map +0 -0
  490. /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.js +0 -0
  491. /package/{dist → packages/core/dist}/cli/utils/settings-consolidation.test.js.map +0 -0
  492. /package/{dist → packages/core/dist}/cli/utils/settings.d.ts +0 -0
  493. /package/{dist → packages/core/dist}/cli/utils/symlinks.d.ts +0 -0
  494. /package/{dist → packages/core/dist}/cli/utils/symlinks.d.ts.map +0 -0
  495. /package/{dist → packages/core/dist}/cli/utils/symlinks.js +0 -0
  496. /package/{dist → packages/core/dist}/cli/utils/symlinks.js.map +0 -0
  497. /package/{dist → packages/core/dist}/cli/utils/themes.js +0 -0
  498. /package/{dist → packages/core/dist}/cli/utils/themes.test.d.ts +0 -0
  499. /package/{dist → packages/core/dist}/cli/utils/themes.test.d.ts.map +0 -0
  500. /package/{dist → packages/core/dist}/cli/utils/themes.test.js +0 -0
  501. /package/{dist → packages/core/dist}/cli/utils/themes.test.js.map +0 -0
  502. /package/{dist → packages/core/dist}/cli/utils/version.d.ts +0 -0
  503. /package/{dist → packages/core/dist}/cli/utils/version.d.ts.map +0 -0
  504. /package/{dist → packages/core/dist}/cli/utils/version.js +0 -0
  505. /package/{dist → packages/core/dist}/cli/utils/version.js.map +0 -0
  506. /package/{dist → packages/core/dist}/cli/workspace.test.d.ts +0 -0
  507. /package/{dist → packages/core/dist}/cli/workspace.test.d.ts.map +0 -0
  508. /package/{dist → packages/core/dist}/cli/workspace.test.js +0 -0
  509. /package/{dist → packages/core/dist}/cli/workspace.test.js.map +0 -0
  510. /package/{dist → packages/core/dist}/index.d.ts +0 -0
  511. /package/{dist → packages/core/dist}/index.d.ts.map +0 -0
  512. /package/{dist → packages/core/dist}/index.js +0 -0
  513. /package/{dist → packages/core/dist}/index.js.map +0 -0
  514. /package/{dist → packages/core/dist}/jira/jira-epic-creation.d.ts +0 -0
  515. /package/{dist → packages/core/dist}/jira/jira-epic-creation.d.ts.map +0 -0
  516. /package/{dist → packages/core/dist}/jira/jira-epic-creation.js +0 -0
  517. /package/{dist → packages/core/dist}/jira/jira-epic-creation.js.map +0 -0
  518. /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.d.ts +0 -0
  519. /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.d.ts.map +0 -0
  520. /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.js +0 -0
  521. /package/{dist → packages/core/dist}/jira/jira-epic-creation.test.js.map +0 -0
  522. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.d.ts +0 -0
  523. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.d.ts.map +0 -0
  524. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.js +0 -0
  525. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.js.map +0 -0
  526. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.d.ts +0 -0
  527. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.d.ts.map +0 -0
  528. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.js +0 -0
  529. /package/{dist → packages/core/dist}/jira/jira-sprint-sync.test.js.map +0 -0
  530. /package/{dist → packages/core/dist}/permissions/index.d.ts +0 -0
  531. /package/{dist → packages/core/dist}/permissions/index.d.ts.map +0 -0
  532. /package/{dist → packages/core/dist}/permissions/index.js +0 -0
  533. /package/{dist → packages/core/dist}/permissions/index.js.map +0 -0
  534. /package/{dist → packages/core/dist}/permissions/permission-schema.d.ts +0 -0
  535. /package/{dist → packages/core/dist}/permissions/permission-schema.d.ts.map +0 -0
  536. /package/{dist → packages/core/dist}/permissions/permission-schema.js +0 -0
  537. /package/{dist → packages/core/dist}/permissions/permission-schema.js.map +0 -0
  538. /package/{dist → packages/core/dist}/permissions/permission-schema.test.d.ts +0 -0
  539. /package/{dist → packages/core/dist}/permissions/permission-schema.test.d.ts.map +0 -0
  540. /package/{dist → packages/core/dist}/permissions/permission-schema.test.js +0 -0
  541. /package/{dist → packages/core/dist}/permissions/permission-schema.test.js.map +0 -0
  542. /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.d.ts +0 -0
  543. /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.d.ts.map +0 -0
  544. /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.js +0 -0
  545. /package/{dist → packages/core/dist}/scripts/add-ocean-profiles.js.map +0 -0
  546. /package/{dist → packages/core/dist}/scripts/benchmark-integration.d.ts +0 -0
  547. /package/{dist → packages/core/dist}/scripts/benchmark-integration.d.ts.map +0 -0
  548. /package/{dist → packages/core/dist}/scripts/benchmark-integration.js +0 -0
  549. /package/{dist → packages/core/dist}/scripts/benchmark-integration.js.map +0 -0
  550. /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.d.ts +0 -0
  551. /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.d.ts.map +0 -0
  552. /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.js +0 -0
  553. /package/{dist → packages/core/dist}/scripts/benchmark-integration.test.js.map +0 -0
  554. /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.d.ts +0 -0
  555. /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.d.ts.map +0 -0
  556. /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.js +0 -0
  557. /package/{dist → packages/core/dist}/scripts/debugging-scenarios.test.js.map +0 -0
  558. /package/{dist → packages/core/dist}/scripts/generate-all-spiders.d.ts +0 -0
  559. /package/{dist → packages/core/dist}/scripts/generate-all-spiders.d.ts.map +0 -0
  560. /package/{dist → packages/core/dist}/scripts/generate-all-spiders.js +0 -0
  561. /package/{dist → packages/core/dist}/scripts/generate-all-spiders.js.map +0 -0
  562. /package/{dist → packages/core/dist}/scripts/generate-report.d.ts +0 -0
  563. /package/{dist → packages/core/dist}/scripts/generate-report.test.d.ts +0 -0
  564. /package/{dist → packages/core/dist}/scripts/generate-report.test.d.ts.map +0 -0
  565. /package/{dist → packages/core/dist}/scripts/generate-report.test.js +0 -0
  566. /package/{dist → packages/core/dist}/scripts/generate-report.test.js.map +0 -0
  567. /package/{dist → packages/core/dist}/scripts/generate-spider-report.d.ts +0 -0
  568. /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.d.ts +0 -0
  569. /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.d.ts.map +0 -0
  570. /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.js +0 -0
  571. /package/{dist → packages/core/dist}/scripts/generate-spider-report.test.js.map +0 -0
  572. /package/{dist → packages/core/dist}/scripts/generate-spider.d.ts +0 -0
  573. /package/{dist → packages/core/dist}/scripts/generate-spider.test.d.ts +0 -0
  574. /package/{dist → packages/core/dist}/scripts/generate-spider.test.d.ts.map +0 -0
  575. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.d.ts +0 -0
  576. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.d.ts.map +0 -0
  577. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.js +0 -0
  578. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.js.map +0 -0
  579. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.d.ts +0 -0
  580. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.d.ts.map +0 -0
  581. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.js +0 -0
  582. /package/{dist → packages/core/dist}/scripts/job-fair-aggregator.test.js.map +0 -0
  583. /package/{dist → packages/core/dist}/scripts/run-ci.test.d.ts +0 -0
  584. /package/{dist → packages/core/dist}/scripts/run-ci.test.d.ts.map +0 -0
  585. /package/{dist → packages/core/dist}/scripts/run-ci.test.js +0 -0
  586. /package/{dist → packages/core/dist}/scripts/run-ci.test.js.map +0 -0
  587. /package/{dist → packages/core/dist}/scripts/theme-detail.test.d.ts +0 -0
  588. /package/{dist → packages/core/dist}/scripts/theme-detail.test.d.ts.map +0 -0
  589. /package/{dist → packages/core/dist}/scripts/theme-detail.test.js +0 -0
  590. /package/{dist → packages/core/dist}/scripts/theme-detail.test.js.map +0 -0
  591. /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.d.ts +0 -0
  592. /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.d.ts.map +0 -0
  593. /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.js +0 -0
  594. /package/{dist → packages/core/dist}/scripts/validate-ocean-profiles.js.map +0 -0
  595. /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.d.ts +0 -0
  596. /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.d.ts.map +0 -0
  597. /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.js +0 -0
  598. /package/{dist → packages/core/dist}/workflow/complete-step-integration.test.js.map +0 -0
  599. /package/{dist → packages/core/dist}/workflow/gate-handler.d.ts +0 -0
  600. /package/{dist → packages/core/dist}/workflow/gate-handler.d.ts.map +0 -0
  601. /package/{dist → packages/core/dist}/workflow/gate-handler.js +0 -0
  602. /package/{dist → packages/core/dist}/workflow/gate-handler.js.map +0 -0
  603. /package/{dist → packages/core/dist}/workflow/gate-handler.test.d.ts +0 -0
  604. /package/{dist → packages/core/dist}/workflow/gate-handler.test.d.ts.map +0 -0
  605. /package/{dist → packages/core/dist}/workflow/gate-handler.test.js +0 -0
  606. /package/{dist → packages/core/dist}/workflow/gate-handler.test.js.map +0 -0
  607. /package/{dist → packages/core/dist}/workflow/generic-sm-finish.d.ts +0 -0
  608. /package/{dist → packages/core/dist}/workflow/generic-sm-finish.d.ts.map +0 -0
  609. /package/{dist → packages/core/dist}/workflow/generic-sm-finish.js +0 -0
  610. /package/{dist → packages/core/dist}/workflow/generic-sm-finish.js.map +0 -0
  611. /package/{dist → packages/core/dist}/workflow/generic-sm-setup.d.ts +0 -0
  612. /package/{dist → packages/core/dist}/workflow/generic-sm-setup.d.ts.map +0 -0
  613. /package/{dist → packages/core/dist}/workflow/generic-sm-setup.js +0 -0
  614. /package/{dist → packages/core/dist}/workflow/generic-sm-setup.js.map +0 -0
  615. /package/{dist → packages/core/dist}/workflow/handoff.d.ts +0 -0
  616. /package/{dist → packages/core/dist}/workflow/handoff.d.ts.map +0 -0
  617. /package/{dist → packages/core/dist}/workflow/handoff.js +0 -0
  618. /package/{dist → packages/core/dist}/workflow/handoff.js.map +0 -0
  619. /package/{dist → packages/core/dist}/workflow/handoff.test.d.ts +0 -0
  620. /package/{dist → packages/core/dist}/workflow/handoff.test.d.ts.map +0 -0
  621. /package/{dist → packages/core/dist}/workflow/handoff.test.js +0 -0
  622. /package/{dist → packages/core/dist}/workflow/handoff.test.js.map +0 -0
  623. /package/{dist → packages/core/dist}/workflow/index.d.ts +0 -0
  624. /package/{dist → packages/core/dist}/workflow/index.d.ts.map +0 -0
  625. /package/{dist → packages/core/dist}/workflow/index.js +0 -0
  626. /package/{dist → packages/core/dist}/workflow/index.js.map +0 -0
  627. /package/{dist → packages/core/dist}/workflow/session-state.d.ts +0 -0
  628. /package/{dist → packages/core/dist}/workflow/session-state.d.ts.map +0 -0
  629. /package/{dist → packages/core/dist}/workflow/session-state.js +0 -0
  630. /package/{dist → packages/core/dist}/workflow/session-state.js.map +0 -0
  631. /package/{dist → packages/core/dist}/workflow/session-state.test.d.ts +0 -0
  632. /package/{dist → packages/core/dist}/workflow/session-state.test.d.ts.map +0 -0
  633. /package/{dist → packages/core/dist}/workflow/session-state.test.js +0 -0
  634. /package/{dist → packages/core/dist}/workflow/session-state.test.js.map +0 -0
  635. /package/{dist → packages/core/dist}/workflow/sm-subagents.test.d.ts +0 -0
  636. /package/{dist → packages/core/dist}/workflow/sm-subagents.test.d.ts.map +0 -0
  637. /package/{dist → packages/core/dist}/workflow/sm-subagents.test.js +0 -0
  638. /package/{dist → packages/core/dist}/workflow/sm-subagents.test.js.map +0 -0
  639. /package/{dist → packages/core/dist}/workflow/step-parser.d.ts +0 -0
  640. /package/{dist → packages/core/dist}/workflow/step-parser.d.ts.map +0 -0
  641. /package/{dist → packages/core/dist}/workflow/step-parser.js +0 -0
  642. /package/{dist → packages/core/dist}/workflow/step-parser.js.map +0 -0
  643. /package/{dist → packages/core/dist}/workflow/step-parser.test.d.ts +0 -0
  644. /package/{dist → packages/core/dist}/workflow/step-parser.test.d.ts.map +0 -0
  645. /package/{dist → packages/core/dist}/workflow/step-parser.test.js +0 -0
  646. /package/{dist → packages/core/dist}/workflow/step-parser.test.js.map +0 -0
  647. /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.d.ts +0 -0
  648. /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.d.ts.map +0 -0
  649. /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.js +0 -0
  650. /package/{dist → packages/core/dist}/workflow/story-workflow-routing.test.js.map +0 -0
  651. /package/{dist → packages/core/dist}/workflow/test-cache.d.ts +0 -0
  652. /package/{dist → packages/core/dist}/workflow/test-cache.d.ts.map +0 -0
  653. /package/{dist → packages/core/dist}/workflow/test-cache.js +0 -0
  654. /package/{dist → packages/core/dist}/workflow/test-cache.js.map +0 -0
  655. /package/{dist → packages/core/dist}/workflow/test-cache.test.d.ts +0 -0
  656. /package/{dist → packages/core/dist}/workflow/test-cache.test.d.ts.map +0 -0
  657. /package/{dist → packages/core/dist}/workflow/test-cache.test.js +0 -0
  658. /package/{dist → packages/core/dist}/workflow/test-cache.test.js.map +0 -0
  659. /package/{dist → packages/core/dist}/workflow/trimodal.d.ts +0 -0
  660. /package/{dist → packages/core/dist}/workflow/trimodal.d.ts.map +0 -0
  661. /package/{dist → packages/core/dist}/workflow/trimodal.js +0 -0
  662. /package/{dist → packages/core/dist}/workflow/trimodal.js.map +0 -0
  663. /package/{dist → packages/core/dist}/workflow/trimodal.test.d.ts +0 -0
  664. /package/{dist → packages/core/dist}/workflow/trimodal.test.d.ts.map +0 -0
  665. /package/{dist → packages/core/dist}/workflow/trimodal.test.js +0 -0
  666. /package/{dist → packages/core/dist}/workflow/trimodal.test.js.map +0 -0
  667. /package/{dist → packages/core/dist}/workflow/variable-resolver.d.ts +0 -0
  668. /package/{dist → packages/core/dist}/workflow/variable-resolver.d.ts.map +0 -0
  669. /package/{dist → packages/core/dist}/workflow/variable-resolver.js +0 -0
  670. /package/{dist → packages/core/dist}/workflow/variable-resolver.js.map +0 -0
  671. /package/{dist → packages/core/dist}/workflow/variable-resolver.test.d.ts +0 -0
  672. /package/{dist → packages/core/dist}/workflow/variable-resolver.test.d.ts.map +0 -0
  673. /package/{dist → packages/core/dist}/workflow/variable-resolver.test.js +0 -0
  674. /package/{dist → packages/core/dist}/workflow/variable-resolver.test.js.map +0 -0
  675. /package/{dist → packages/core/dist}/workflow/workflow-executor.d.ts +0 -0
  676. /package/{dist → packages/core/dist}/workflow/workflow-executor.d.ts.map +0 -0
  677. /package/{dist → packages/core/dist}/workflow/workflow-executor.js +0 -0
  678. /package/{dist → packages/core/dist}/workflow/workflow-executor.js.map +0 -0
  679. /package/{dist → packages/core/dist}/workflow/workflow-executor.test.d.ts +0 -0
  680. /package/{dist → packages/core/dist}/workflow/workflow-executor.test.d.ts.map +0 -0
  681. /package/{dist → packages/core/dist}/workflow/workflow-executor.test.js +0 -0
  682. /package/{dist → packages/core/dist}/workflow/workflow-executor.test.js.map +0 -0
  683. /package/{dist → packages/core/dist}/workflow/workflow-loader.d.ts +0 -0
  684. /package/{dist → packages/core/dist}/workflow/workflow-loader.d.ts.map +0 -0
  685. /package/{dist → packages/core/dist}/workflow/workflow-loader.js +0 -0
  686. /package/{dist → packages/core/dist}/workflow/workflow-loader.js.map +0 -0
  687. /package/{dist → packages/core/dist}/workflow/workflow-loader.test.d.ts +0 -0
  688. /package/{dist → packages/core/dist}/workflow/workflow-loader.test.d.ts.map +0 -0
  689. /package/{dist → packages/core/dist}/workflow/workflow-loader.test.js +0 -0
  690. /package/{dist → packages/core/dist}/workflow/workflow-loader.test.js.map +0 -0
  691. /package/{dist → packages/core/dist}/workflow/workflow-migration.test.d.ts +0 -0
  692. /package/{dist → packages/core/dist}/workflow/workflow-migration.test.d.ts.map +0 -0
  693. /package/{dist → packages/core/dist}/workflow/workflow-migration.test.js +0 -0
  694. /package/{dist → packages/core/dist}/workflow/workflow-migration.test.js.map +0 -0
  695. /package/{dist → packages/core/dist}/workflow/workflow-permissions.d.ts +0 -0
  696. /package/{dist → packages/core/dist}/workflow/workflow-permissions.d.ts.map +0 -0
  697. /package/{dist → packages/core/dist}/workflow/workflow-permissions.js +0 -0
  698. /package/{dist → packages/core/dist}/workflow/workflow-permissions.js.map +0 -0
  699. /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.d.ts +0 -0
  700. /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.d.ts.map +0 -0
  701. /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.js +0 -0
  702. /package/{dist → packages/core/dist}/workflow/workflow-permissions.test.js.map +0 -0
  703. /package/{dist → packages/core/dist}/workflow/workflow-router.d.ts +0 -0
  704. /package/{dist → packages/core/dist}/workflow/workflow-router.d.ts.map +0 -0
  705. /package/{dist → packages/core/dist}/workflow/workflow-router.js +0 -0
  706. /package/{dist → packages/core/dist}/workflow/workflow-router.js.map +0 -0
  707. /package/{dist → packages/core/dist}/workflow/workflow-router.test.d.ts +0 -0
  708. /package/{dist → packages/core/dist}/workflow/workflow-router.test.d.ts.map +0 -0
  709. /package/{dist → packages/core/dist}/workflow/workflow-router.test.js +0 -0
  710. /package/{dist → packages/core/dist}/workflow/workflow-router.test.js.map +0 -0
  711. /package/{dist → packages/core/dist}/workflow/workflow-schema.d.ts +0 -0
  712. /package/{dist → packages/core/dist}/workflow/workflow-schema.d.ts.map +0 -0
  713. /package/{dist → packages/core/dist}/workflow/workflow-schema.js +0 -0
  714. /package/{dist → packages/core/dist}/workflow/workflow-schema.js.map +0 -0
  715. /package/{dist → packages/core/dist}/workflow/workflow-schema.test.d.ts +0 -0
  716. /package/{dist → packages/core/dist}/workflow/workflow-schema.test.d.ts.map +0 -0
  717. /package/{dist → packages/core/dist}/workflow/workflow-schema.test.js +0 -0
  718. /package/{dist → packages/core/dist}/workflow/workflow-schema.test.js.map +0 -0
  719. /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.d.ts +0 -0
  720. /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.d.ts.map +0 -0
  721. /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.js +0 -0
  722. /package/{dist → packages/core/dist}/workflow/workflow-stepped-schema.test.js.map +0 -0
@@ -0,0 +1,815 @@
1
+ """Tests for sprint/yaml_io.py module.
2
+
3
+ Story: MSSCI-14254 - Core yaml_io module with deterministic serialization
4
+
5
+ TDD RED phase: All tests should FAIL until implementation.
6
+
7
+ Acceptance Criteria:
8
+ 1. Same input produces byte-identical output
9
+ 2. Round-trip integrity (read -> write -> read = identical)
10
+ 3. Atomic writes prevent partial file corruption
11
+ 4. Key ordering matches sprint-template.yaml
12
+ """
13
+
14
+ import os
15
+ from pathlib import Path
16
+ from typing import Any
17
+ from unittest.mock import patch
18
+
19
+ import pytest
20
+
21
+ from pennyfarthing_scripts.sprint.yaml_io import (
22
+ EPIC_KEY_ORDER,
23
+ SPRINT_KEY_ORDER,
24
+ STORY_KEY_ORDER,
25
+ canonical_dump,
26
+ read_sprint,
27
+ write_sprint,
28
+ )
29
+
30
+
31
+ # =============================================================================
32
+ # Test Fixtures
33
+ # =============================================================================
34
+
35
+
36
+ MINIMAL_SPRINT_YAML = """\
37
+ sprint:
38
+ name: "TO Sprint 2604"
39
+ jira_sprint_id: 276
40
+ jira_sprint_name: "TO Sprint 2604"
41
+ goal: Complete the sprint
42
+ start_date: 2026-01-20
43
+ end_date: 2026-02-02
44
+ status: active
45
+ epics: []
46
+ """
47
+
48
+ FULL_SPRINT_YAML = """\
49
+ sprint:
50
+ name: "TO Sprint 2604"
51
+ jira_sprint_id: 276
52
+ jira_sprint_name: "TO Sprint 2604"
53
+ goal: Complete the sprint
54
+ start_date: 2026-01-20
55
+ end_date: 2026-02-02
56
+ status: active
57
+ epics:
58
+ - id: epic-63
59
+ type: epic
60
+ title: "Epic: Test Epic"
61
+ description: |
62
+ This is a test epic with a multiline description.
63
+ It has multiple lines.
64
+ priority: P1
65
+ status: in_progress
66
+ repos: pennyfarthing
67
+ jira: MSSCI-12000
68
+ points: 8
69
+ stories:
70
+ - id: 63-1
71
+ jira: MSSCI-12001
72
+ title: First Story
73
+ description: |
74
+ A story with a multiline description.
75
+ points: 3
76
+ priority: P0
77
+ status: backlog
78
+ repos: pennyfarthing
79
+ workflow: tdd
80
+ acceptance_criteria:
81
+ - First criterion
82
+ - Second criterion
83
+ - id: 63-2
84
+ jira: MSSCI-12002
85
+ title: Second Story
86
+ points: 5
87
+ priority: P1
88
+ status: in_progress
89
+ assigned_to: keithavery
90
+ started: "2026-01-21T10:00:00Z"
91
+ repos: pennyfarthing
92
+ workflow: trivial
93
+ """
94
+
95
+
96
+ @pytest.fixture
97
+ def minimal_sprint_file(tmp_path: Path) -> Path:
98
+ """Create a minimal valid sprint YAML file."""
99
+ p = tmp_path / "current-sprint.yaml"
100
+ p.write_text(MINIMAL_SPRINT_YAML)
101
+ return p
102
+
103
+
104
+ @pytest.fixture
105
+ def full_sprint_file(tmp_path: Path) -> Path:
106
+ """Create a full sprint YAML file with epics and stories."""
107
+ p = tmp_path / "current-sprint.yaml"
108
+ p.write_text(FULL_SPRINT_YAML)
109
+ return p
110
+
111
+
112
+ @pytest.fixture
113
+ def sprint_data_dict() -> dict[str, Any]:
114
+ """Sprint data as a plain dict (unordered)."""
115
+ return {
116
+ "sprint": {
117
+ "status": "active",
118
+ "name": "TO Sprint 2604",
119
+ "end_date": "2026-02-02",
120
+ "goal": "Complete the sprint",
121
+ "jira_sprint_id": 276,
122
+ "start_date": "2026-01-20",
123
+ "jira_sprint_name": "TO Sprint 2604",
124
+ },
125
+ "epics": [],
126
+ }
127
+
128
+
129
+ @pytest.fixture
130
+ def scrambled_keys_file(tmp_path: Path) -> Path:
131
+ """Sprint YAML with keys in wrong order."""
132
+ content = """\
133
+ sprint:
134
+ status: active
135
+ name: "TO Sprint 2604"
136
+ end_date: 2026-02-02
137
+ goal: Complete the sprint
138
+ jira_sprint_id: 276
139
+ start_date: 2026-01-20
140
+ jira_sprint_name: "TO Sprint 2604"
141
+ epics:
142
+ - stories: []
143
+ id: epic-63
144
+ status: in_progress
145
+ title: "Epic: Test"
146
+ priority: P1
147
+ """
148
+ p = tmp_path / "scrambled.yaml"
149
+ p.write_text(content)
150
+ return p
151
+
152
+
153
+ # =============================================================================
154
+ # AC1: Same input produces byte-identical output
155
+ # =============================================================================
156
+
157
+
158
+ class TestDeterministicOutput:
159
+ """canonical_dump must produce byte-identical output for the same input."""
160
+
161
+ def test_same_input_same_output(self, full_sprint_file: Path) -> None:
162
+ """Calling canonical_dump twice on the same data produces identical bytes."""
163
+ data = read_sprint(full_sprint_file)
164
+
165
+ output1 = canonical_dump(data)
166
+ output2 = canonical_dump(data)
167
+
168
+ assert output1 == output2
169
+ assert isinstance(output1, str)
170
+
171
+ def test_deterministic_across_reads(self, full_sprint_file: Path) -> None:
172
+ """Reading the same file twice and dumping produces identical output."""
173
+ data1 = read_sprint(full_sprint_file)
174
+ data2 = read_sprint(full_sprint_file)
175
+
176
+ assert canonical_dump(data1) == canonical_dump(data2)
177
+
178
+ def test_no_trailing_whitespace(self, full_sprint_file: Path) -> None:
179
+ """Output should have no trailing whitespace on any line."""
180
+ data = read_sprint(full_sprint_file)
181
+ output = canonical_dump(data)
182
+
183
+ for i, line in enumerate(output.split("\n"), 1):
184
+ assert line == line.rstrip(), (
185
+ f"Line {i} has trailing whitespace: {line!r}"
186
+ )
187
+
188
+ def test_ends_with_single_newline(self, full_sprint_file: Path) -> None:
189
+ """Output should end with exactly one newline."""
190
+ data = read_sprint(full_sprint_file)
191
+ output = canonical_dump(data)
192
+
193
+ assert output.endswith("\n")
194
+ assert not output.endswith("\n\n")
195
+
196
+ def test_two_space_indentation(self, full_sprint_file: Path) -> None:
197
+ """Indentation should use 2 spaces, never tabs."""
198
+ data = read_sprint(full_sprint_file)
199
+ output = canonical_dump(data)
200
+
201
+ assert "\t" not in output
202
+ # Check that indented lines use multiples of 2 spaces
203
+ for line in output.split("\n"):
204
+ if line and line[0] == " ":
205
+ leading = len(line) - len(line.lstrip())
206
+ assert leading % 2 == 0, (
207
+ f"Non-2-space indent ({leading}): {line!r}"
208
+ )
209
+
210
+ def test_multiline_uses_block_scalars(self, full_sprint_file: Path) -> None:
211
+ """Multiline string fields (description) should use block scalar style (|)."""
212
+ data = read_sprint(full_sprint_file)
213
+ output = canonical_dump(data)
214
+
215
+ # The description field has multiple lines, should use block scalar
216
+ assert "description: |" in output or "description: |\n" in output
217
+
218
+ def test_empty_epics_list(self, minimal_sprint_file: Path) -> None:
219
+ """Empty epics list should serialize deterministically."""
220
+ data = read_sprint(minimal_sprint_file)
221
+
222
+ output1 = canonical_dump(data)
223
+ output2 = canonical_dump(data)
224
+
225
+ assert output1 == output2
226
+ assert "epics:" in output1
227
+
228
+
229
+ # =============================================================================
230
+ # AC2: Round-trip integrity (read -> write -> read = identical)
231
+ # =============================================================================
232
+
233
+
234
+ class TestRoundTripIntegrity:
235
+ """read_sprint -> write_sprint -> read_sprint must produce identical data."""
236
+
237
+ def test_minimal_round_trip(self, tmp_path: Path, minimal_sprint_file: Path) -> None:
238
+ """Minimal sprint YAML survives a read-write-read cycle."""
239
+ data1 = read_sprint(minimal_sprint_file)
240
+
241
+ out_path = tmp_path / "output.yaml"
242
+ write_sprint(out_path, data1)
243
+ data2 = read_sprint(out_path)
244
+
245
+ # Data must be structurally identical
246
+ assert canonical_dump(data1) == canonical_dump(data2)
247
+
248
+ def test_full_round_trip(self, tmp_path: Path, full_sprint_file: Path) -> None:
249
+ """Full sprint YAML with epics/stories survives a read-write-read cycle."""
250
+ data1 = read_sprint(full_sprint_file)
251
+
252
+ out_path = tmp_path / "output.yaml"
253
+ write_sprint(out_path, data1)
254
+ data2 = read_sprint(out_path)
255
+
256
+ assert canonical_dump(data1) == canonical_dump(data2)
257
+
258
+ def test_round_trip_preserves_multiline_strings(
259
+ self, tmp_path: Path, full_sprint_file: Path
260
+ ) -> None:
261
+ """Multiline description fields survive round-trip intact."""
262
+ data1 = read_sprint(full_sprint_file)
263
+ epic = data1["epics"][0]
264
+ original_desc = str(epic["description"])
265
+
266
+ out_path = tmp_path / "output.yaml"
267
+ write_sprint(out_path, data1)
268
+ data2 = read_sprint(out_path)
269
+
270
+ assert str(data2["epics"][0]["description"]) == original_desc
271
+
272
+ def test_round_trip_preserves_list_fields(
273
+ self, tmp_path: Path, full_sprint_file: Path
274
+ ) -> None:
275
+ """acceptance_criteria lists survive round-trip."""
276
+ data1 = read_sprint(full_sprint_file)
277
+ story = data1["epics"][0]["stories"][0]
278
+ original_ac = list(story["acceptance_criteria"])
279
+
280
+ out_path = tmp_path / "output.yaml"
281
+ write_sprint(out_path, data1)
282
+ data2 = read_sprint(out_path)
283
+
284
+ assert list(data2["epics"][0]["stories"][0]["acceptance_criteria"]) == original_ac
285
+
286
+ def test_round_trip_preserves_integer_fields(
287
+ self, tmp_path: Path, full_sprint_file: Path
288
+ ) -> None:
289
+ """Integer fields (points, jira_sprint_id) stay as integers."""
290
+ data1 = read_sprint(full_sprint_file)
291
+
292
+ out_path = tmp_path / "output.yaml"
293
+ write_sprint(out_path, data1)
294
+ data2 = read_sprint(out_path)
295
+
296
+ assert data2["sprint"]["jira_sprint_id"] == 276
297
+ assert isinstance(data2["sprint"]["jira_sprint_id"], int)
298
+ assert data2["epics"][0]["stories"][0]["points"] == 3
299
+ assert isinstance(data2["epics"][0]["stories"][0]["points"], int)
300
+
301
+ def test_round_trip_preserves_date_strings(
302
+ self, tmp_path: Path, full_sprint_file: Path
303
+ ) -> None:
304
+ """Date fields stay as strings, not datetime objects."""
305
+ data1 = read_sprint(full_sprint_file)
306
+
307
+ out_path = tmp_path / "output.yaml"
308
+ write_sprint(out_path, data1)
309
+ data2 = read_sprint(out_path)
310
+
311
+ # Dates should be strings, not datetime objects
312
+ start = data2["sprint"]["start_date"]
313
+ assert isinstance(start, str) or hasattr(start, '__str__')
314
+ assert str(start) == "2026-01-20"
315
+
316
+ def test_double_round_trip_stable(
317
+ self, tmp_path: Path, full_sprint_file: Path
318
+ ) -> None:
319
+ """Two consecutive round-trips produce byte-identical output."""
320
+ data1 = read_sprint(full_sprint_file)
321
+
322
+ path_a = tmp_path / "a.yaml"
323
+ write_sprint(path_a, data1)
324
+
325
+ data2 = read_sprint(path_a)
326
+ path_b = tmp_path / "b.yaml"
327
+ write_sprint(path_b, data2)
328
+
329
+ assert path_a.read_text() == path_b.read_text()
330
+
331
+ def test_round_trip_with_real_sprint_file(self, tmp_path: Path) -> None:
332
+ """Round-trip the actual current-sprint.yaml if available."""
333
+ project_root = Path(__file__).parent.parent.parent
334
+ real_file = project_root / "sprint" / "current-sprint.yaml"
335
+
336
+ if not real_file.exists():
337
+ pytest.skip("No current-sprint.yaml available")
338
+
339
+ data1 = read_sprint(real_file)
340
+
341
+ out_path = tmp_path / "round-trip.yaml"
342
+ write_sprint(out_path, data1)
343
+ data2 = read_sprint(out_path)
344
+
345
+ assert canonical_dump(data1) == canonical_dump(data2)
346
+
347
+
348
+ # =============================================================================
349
+ # AC3: Atomic writes prevent partial file corruption
350
+ # =============================================================================
351
+
352
+
353
+ class TestAtomicWrites:
354
+ """write_sprint must use atomic write (temp file + os.replace)."""
355
+
356
+ def test_write_creates_file(self, tmp_path: Path, minimal_sprint_file: Path) -> None:
357
+ """write_sprint should create the output file."""
358
+ data = read_sprint(minimal_sprint_file)
359
+ out_path = tmp_path / "output.yaml"
360
+
361
+ write_sprint(out_path, data)
362
+
363
+ assert out_path.exists()
364
+ assert out_path.stat().st_size > 0
365
+
366
+ def test_write_overwrites_existing(
367
+ self, tmp_path: Path, minimal_sprint_file: Path
368
+ ) -> None:
369
+ """write_sprint should overwrite an existing file."""
370
+ out_path = tmp_path / "output.yaml"
371
+ out_path.write_text("old content")
372
+
373
+ data = read_sprint(minimal_sprint_file)
374
+ write_sprint(out_path, data)
375
+
376
+ content = out_path.read_text()
377
+ assert "old content" not in content
378
+ assert "sprint:" in content
379
+
380
+ def test_no_temp_file_left_on_success(
381
+ self, tmp_path: Path, minimal_sprint_file: Path
382
+ ) -> None:
383
+ """No .yaml.tmp file should remain after successful write."""
384
+ data = read_sprint(minimal_sprint_file)
385
+ out_path = tmp_path / "output.yaml"
386
+
387
+ write_sprint(out_path, data)
388
+
389
+ tmp_file = out_path.with_suffix(".yaml.tmp")
390
+ assert not tmp_file.exists()
391
+
392
+ def test_original_preserved_on_dump_failure(
393
+ self, tmp_path: Path, minimal_sprint_file: Path
394
+ ) -> None:
395
+ """If serialization fails, the original file must be untouched."""
396
+ out_path = tmp_path / "output.yaml"
397
+ original_content = "original: content\n"
398
+ out_path.write_text(original_content)
399
+
400
+ # Pass something that can't be serialized
401
+ with pytest.raises((TypeError, ValueError, Exception)):
402
+ write_sprint(out_path, object())
403
+
404
+ assert out_path.read_text() == original_content
405
+
406
+ def test_write_to_nonexistent_directory_fails(self) -> None:
407
+ """Writing to a non-existent directory should raise an error."""
408
+ bad_path = Path("/nonexistent/dir/sprint.yaml")
409
+
410
+ with pytest.raises((OSError, FileNotFoundError)):
411
+ write_sprint(bad_path, {"sprint": {}, "epics": []})
412
+
413
+ def test_write_uses_same_directory_for_temp(
414
+ self, tmp_path: Path, minimal_sprint_file: Path
415
+ ) -> None:
416
+ """Temp file should be in the same directory as target (for atomic rename)."""
417
+ data = read_sprint(minimal_sprint_file)
418
+ out_path = tmp_path / "subdir" / "sprint.yaml"
419
+ out_path.parent.mkdir(parents=True)
420
+
421
+ write_sprint(out_path, data)
422
+
423
+ # If temp was in a different filesystem, os.replace would fail
424
+ # Success here means temp was in same directory
425
+ assert out_path.exists()
426
+
427
+ def test_write_file_is_valid_yaml(
428
+ self, tmp_path: Path, full_sprint_file: Path
429
+ ) -> None:
430
+ """Written file must be parseable as valid YAML."""
431
+ import yaml
432
+
433
+ data = read_sprint(full_sprint_file)
434
+ out_path = tmp_path / "output.yaml"
435
+
436
+ write_sprint(out_path, data)
437
+
438
+ # Should be parseable by standard PyYAML
439
+ with open(out_path) as f:
440
+ parsed = yaml.safe_load(f)
441
+
442
+ assert isinstance(parsed, dict)
443
+ assert "sprint" in parsed
444
+ assert "epics" in parsed
445
+
446
+
447
+ # =============================================================================
448
+ # AC4: Key ordering matches sprint-template.yaml
449
+ # =============================================================================
450
+
451
+
452
+ class TestKeyOrdering:
453
+ """Keys in output must follow sprint-template.yaml ordering."""
454
+
455
+ def test_sprint_key_order_constant_defined(self) -> None:
456
+ """SPRINT_KEY_ORDER constant should list expected sprint keys."""
457
+ assert "name" in SPRINT_KEY_ORDER
458
+ assert "jira_sprint_id" in SPRINT_KEY_ORDER
459
+ assert "goal" in SPRINT_KEY_ORDER
460
+ assert "status" in SPRINT_KEY_ORDER
461
+ # name should come before status
462
+ assert SPRINT_KEY_ORDER.index("name") < SPRINT_KEY_ORDER.index("status")
463
+
464
+ def test_epic_key_order_constant_defined(self) -> None:
465
+ """EPIC_KEY_ORDER constant should list expected epic keys."""
466
+ assert "id" in EPIC_KEY_ORDER
467
+ assert "title" in EPIC_KEY_ORDER
468
+ assert "stories" in EPIC_KEY_ORDER
469
+ # id should come first, stories last
470
+ assert EPIC_KEY_ORDER.index("id") == 0
471
+ assert EPIC_KEY_ORDER.index("stories") == len(EPIC_KEY_ORDER) - 1
472
+
473
+ def test_story_key_order_constant_defined(self) -> None:
474
+ """STORY_KEY_ORDER constant should list expected story keys."""
475
+ assert "id" in STORY_KEY_ORDER
476
+ assert "title" in STORY_KEY_ORDER
477
+ assert "points" in STORY_KEY_ORDER
478
+ assert "acceptance_criteria" in STORY_KEY_ORDER
479
+ # id should come first
480
+ assert STORY_KEY_ORDER.index("id") == 0
481
+ # acceptance_criteria after status
482
+ assert STORY_KEY_ORDER.index("acceptance_criteria") > STORY_KEY_ORDER.index("status")
483
+
484
+ def test_sprint_keys_in_canonical_order(self, full_sprint_file: Path) -> None:
485
+ """Sprint section keys should follow SPRINT_KEY_ORDER."""
486
+ data = read_sprint(full_sprint_file)
487
+ output = canonical_dump(data)
488
+
489
+ # Extract sprint section key positions
490
+ lines = output.split("\n")
491
+ sprint_keys = []
492
+ in_sprint = False
493
+ for line in lines:
494
+ if line.startswith("sprint:"):
495
+ in_sprint = True
496
+ continue
497
+ if in_sprint and line and not line.startswith(" "):
498
+ break
499
+ if in_sprint and line.startswith(" ") and ":" in line:
500
+ key = line.strip().split(":")[0]
501
+ sprint_keys.append(key)
502
+
503
+ # Verify ordering matches SPRINT_KEY_ORDER (for keys that are present)
504
+ expected_order = [k for k in SPRINT_KEY_ORDER if k in sprint_keys]
505
+ assert sprint_keys == expected_order
506
+
507
+ def test_epic_keys_in_canonical_order(self, full_sprint_file: Path) -> None:
508
+ """Epic section keys should follow EPIC_KEY_ORDER."""
509
+ data = read_sprint(full_sprint_file)
510
+ output = canonical_dump(data)
511
+
512
+ # Parse to find epic keys in order
513
+ lines = output.split("\n")
514
+ epic_keys = []
515
+ in_epic = False
516
+ indent_level = None
517
+ for line in lines:
518
+ stripped = line.lstrip()
519
+ current_indent = len(line) - len(stripped)
520
+
521
+ if stripped.startswith("- id:") and "epic" in str(line):
522
+ in_epic = True
523
+ indent_level = current_indent + 2 # keys are 2 spaces after "- "
524
+ epic_keys.append("id")
525
+ continue
526
+
527
+ if in_epic:
528
+ if stripped and current_indent < indent_level and not stripped.startswith("- "):
529
+ break
530
+ if current_indent == indent_level and ":" in stripped:
531
+ key = stripped.split(":")[0]
532
+ if key != "-":
533
+ epic_keys.append(key)
534
+ # Stop at stories section content
535
+ if stripped.startswith("stories:"):
536
+ break
537
+
538
+ expected_order = [k for k in EPIC_KEY_ORDER if k in epic_keys]
539
+ assert epic_keys == expected_order
540
+
541
+ def test_story_keys_in_canonical_order(self, full_sprint_file: Path) -> None:
542
+ """Story keys should follow STORY_KEY_ORDER."""
543
+ data = read_sprint(full_sprint_file)
544
+ output = canonical_dump(data)
545
+
546
+ # Find first story's keys
547
+ lines = output.split("\n")
548
+ story_keys = []
549
+ in_stories = False
550
+ in_story = False
551
+ story_indent = None
552
+ for line in lines:
553
+ stripped = line.lstrip()
554
+ current_indent = len(line) - len(stripped)
555
+
556
+ if "stories:" in line:
557
+ in_stories = True
558
+ continue
559
+
560
+ if in_stories and stripped.startswith("- id:"):
561
+ if in_story:
562
+ break # We only want the first story
563
+ in_story = True
564
+ story_indent = current_indent + 2
565
+ story_keys.append("id")
566
+ continue
567
+
568
+ if in_story:
569
+ if stripped and current_indent < story_indent and not stripped.startswith("- "):
570
+ break
571
+ if current_indent == story_indent and ":" in stripped:
572
+ key = stripped.split(":")[0]
573
+ if key != "-":
574
+ story_keys.append(key)
575
+
576
+ expected_order = [k for k in STORY_KEY_ORDER if k in story_keys]
577
+ assert story_keys == expected_order
578
+
579
+ def test_scrambled_keys_reordered(
580
+ self, tmp_path: Path, scrambled_keys_file: Path
581
+ ) -> None:
582
+ """Reading scrambled keys and dumping should produce canonical order."""
583
+ data = read_sprint(scrambled_keys_file)
584
+ output = canonical_dump(data)
585
+
586
+ # Sprint keys should be reordered
587
+ lines = output.split("\n")
588
+ sprint_keys = []
589
+ in_sprint = False
590
+ for line in lines:
591
+ if line.startswith("sprint:"):
592
+ in_sprint = True
593
+ continue
594
+ if in_sprint and line and not line.startswith(" "):
595
+ break
596
+ if in_sprint and line.startswith(" ") and ":" in line:
597
+ key = line.strip().split(":")[0]
598
+ sprint_keys.append(key)
599
+
600
+ # "name" should come before "status" (template order)
601
+ assert sprint_keys.index("name") < sprint_keys.index("status")
602
+ # "jira_sprint_id" should come before "goal"
603
+ assert sprint_keys.index("jira_sprint_id") < sprint_keys.index("goal")
604
+
605
+ def test_unknown_keys_preserved_at_end(self, tmp_path: Path) -> None:
606
+ """Keys not in the template should be preserved, appended after known keys."""
607
+ content = """\
608
+ sprint:
609
+ name: "TO Sprint 2604"
610
+ jira_sprint_id: 276
611
+ jira_sprint_name: "TO Sprint 2604"
612
+ goal: Test
613
+ start_date: 2026-01-20
614
+ end_date: 2026-02-02
615
+ status: active
616
+ custom_field: extra value
617
+ epics: []
618
+ """
619
+ p = tmp_path / "custom.yaml"
620
+ p.write_text(content)
621
+
622
+ data = read_sprint(p)
623
+ output = canonical_dump(data)
624
+
625
+ # custom_field should still be present
626
+ assert "custom_field" in output
627
+ # And should come after all known keys
628
+ lines = output.split("\n")
629
+ sprint_keys = []
630
+ in_sprint = False
631
+ for line in lines:
632
+ if line.startswith("sprint:"):
633
+ in_sprint = True
634
+ continue
635
+ if in_sprint and line and not line.startswith(" "):
636
+ break
637
+ if in_sprint and line.startswith(" ") and ":" in line:
638
+ key = line.strip().split(":")[0]
639
+ sprint_keys.append(key)
640
+
641
+ assert sprint_keys[-1] == "custom_field"
642
+
643
+
644
+ # =============================================================================
645
+ # read_sprint error handling
646
+ # =============================================================================
647
+
648
+
649
+ class TestReadSprint:
650
+ """Tests for read_sprint function."""
651
+
652
+ def test_read_valid_file(self, minimal_sprint_file: Path) -> None:
653
+ """Should successfully read a valid sprint YAML file."""
654
+ data = read_sprint(minimal_sprint_file)
655
+
656
+ assert data is not None
657
+ assert "sprint" in data
658
+ assert "epics" in data
659
+ assert data["sprint"]["name"] == "TO Sprint 2604"
660
+
661
+ def test_read_nonexistent_file(self) -> None:
662
+ """Should raise FileNotFoundError for missing file."""
663
+ with pytest.raises(FileNotFoundError):
664
+ read_sprint(Path("/nonexistent/file.yaml"))
665
+
666
+ def test_read_malformed_yaml(self, tmp_path: Path) -> None:
667
+ """Should raise ValueError for malformed YAML."""
668
+ bad = tmp_path / "bad.yaml"
669
+ bad.write_text("this: is: not: [valid yaml")
670
+
671
+ with pytest.raises((ValueError, Exception)):
672
+ read_sprint(bad)
673
+
674
+ def test_read_empty_file(self, tmp_path: Path) -> None:
675
+ """Should handle empty file gracefully."""
676
+ empty = tmp_path / "empty.yaml"
677
+ empty.write_text("")
678
+
679
+ with pytest.raises((ValueError, Exception)):
680
+ read_sprint(empty)
681
+
682
+ def test_read_preserves_key_order(self, full_sprint_file: Path) -> None:
683
+ """Reading should preserve the key ordering from the file."""
684
+ data = read_sprint(full_sprint_file)
685
+
686
+ # The returned type should maintain insertion order
687
+ sprint_keys = list(data["sprint"].keys())
688
+ assert sprint_keys[0] == "name"
689
+
690
+ def test_read_preserves_types(self, full_sprint_file: Path) -> None:
691
+ """Reading should preserve correct Python types."""
692
+ data = read_sprint(full_sprint_file)
693
+
694
+ assert isinstance(data["sprint"]["jira_sprint_id"], int)
695
+ assert isinstance(data["sprint"]["name"], str)
696
+ assert isinstance(data["epics"], list)
697
+ assert isinstance(data["epics"][0]["stories"], list)
698
+ assert isinstance(data["epics"][0]["stories"][0]["points"], int)
699
+
700
+
701
+ # =============================================================================
702
+ # Sharded format support
703
+ # =============================================================================
704
+
705
+
706
+ SHARDED_INDEX_YAML = """\
707
+ sprint:
708
+ name: "TO Sprint 2606"
709
+ jira_sprint_id: 309
710
+ jira_sprint_name: "TO Sprint 2606"
711
+ goal: Test sharding
712
+ start_date: 2026-02-02
713
+ end_date: 2026-02-15
714
+ status: active
715
+ epics:
716
+ - MSSCI-14298
717
+ - epic-40
718
+ stories: []
719
+ """
720
+
721
+ SHARD_JIRA_YAML = """\
722
+ id: MSSCI-14298
723
+ type: epic
724
+ title: "Epic: Stepped Workflow"
725
+ priority: P1
726
+ status: in_progress
727
+ jira: MSSCI-14298
728
+ stories:
729
+ - id: MSSCI-14299
730
+ title: Wire up stepped workflow
731
+ points: 5
732
+ priority: P0
733
+ status: done
734
+ """
735
+
736
+ SHARD_INTERNAL_YAML = """\
737
+ id: epic-40
738
+ type: epic
739
+ title: "Epic: Scale Adaptation"
740
+ priority: P2
741
+ status: backlog
742
+ stories:
743
+ - id: 40-1
744
+ title: First story
745
+ points: 3
746
+ priority: P1
747
+ status: backlog
748
+ """
749
+
750
+
751
+ @pytest.fixture
752
+ def sharded_sprint_dir(tmp_path: Path) -> Path:
753
+ """Create a sharded sprint directory structure."""
754
+ (tmp_path / "current-sprint.yaml").write_text(SHARDED_INDEX_YAML)
755
+ (tmp_path / "epic-MSSCI-14298.yaml").write_text(SHARD_JIRA_YAML)
756
+ (tmp_path / "epic-epic-40.yaml").write_text(SHARD_INTERNAL_YAML)
757
+ return tmp_path
758
+
759
+
760
+ class TestShardedReadWrite:
761
+ """Tests for sharded epic format in yaml_io."""
762
+
763
+ def test_read_merges_shards(self, sharded_sprint_dir: Path) -> None:
764
+ """read_sprint should merge shard files into full epics."""
765
+ data = read_sprint(sharded_sprint_dir / "current-sprint.yaml")
766
+
767
+ assert len(data["epics"]) == 2
768
+ assert data["epics"][0]["id"] == "MSSCI-14298"
769
+ assert data["epics"][1]["id"] == "epic-40"
770
+ assert len(data["epics"][0]["stories"]) == 1
771
+ assert len(data["epics"][1]["stories"]) == 1
772
+
773
+ def test_write_preserves_sharded_format(self, sharded_sprint_dir: Path) -> None:
774
+ """write_sprint should write back to shard files when format is sharded."""
775
+ index_path = sharded_sprint_dir / "current-sprint.yaml"
776
+ data = read_sprint(index_path)
777
+
778
+ # Mutate a story
779
+ data["epics"][1]["stories"][0]["status"] = "in_progress"
780
+
781
+ write_sprint(index_path, data)
782
+
783
+ # Index should still have string refs
784
+ import yaml
785
+ with open(index_path) as f:
786
+ raw_index = yaml.safe_load(f)
787
+ assert isinstance(raw_index["epics"][0], str)
788
+ assert raw_index["epics"][0] == "MSSCI-14298"
789
+
790
+ # Shard file should have the updated story
791
+ shard = read_sprint(sharded_sprint_dir / "epic-epic-40.yaml")
792
+ assert shard["stories"][0]["status"] == "in_progress"
793
+
794
+ def test_sharded_round_trip(self, sharded_sprint_dir: Path) -> None:
795
+ """Read-write-read on sharded format should be stable."""
796
+ index_path = sharded_sprint_dir / "current-sprint.yaml"
797
+
798
+ data1 = read_sprint(index_path)
799
+ write_sprint(index_path, data1)
800
+ data2 = read_sprint(index_path)
801
+
802
+ assert canonical_dump(data1) == canonical_dump(data2)
803
+
804
+ def test_non_sharded_write_unchanged(self, tmp_path: Path, full_sprint_file: Path) -> None:
805
+ """write_sprint on non-sharded data should write a single file."""
806
+ data = read_sprint(full_sprint_file)
807
+ out_path = tmp_path / "output.yaml"
808
+
809
+ write_sprint(out_path, data)
810
+
811
+ # Should be a single file, no shard files created
812
+ import yaml
813
+ with open(out_path) as f:
814
+ raw = yaml.safe_load(f)
815
+ assert isinstance(raw["epics"][0], dict) # Full dicts, not refs