@takuma-hirai/hirai-method 0.1.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.
Files changed (822) hide show
  1. package/.claude/.stale-harness-state/last-check +0 -0
  2. package/.claude/CommonRules.md +121 -0
  3. package/.claude/agents/01-core-development/api-designer.md +237 -0
  4. package/.claude/agents/01-core-development/backend-developer.md +222 -0
  5. package/.claude/agents/01-core-development/design-bridge.md +127 -0
  6. package/.claude/agents/01-core-development/electron-pro.md +240 -0
  7. package/.claude/agents/01-core-development/frontend-developer.md +133 -0
  8. package/.claude/agents/01-core-development/fullstack-developer.md +235 -0
  9. package/.claude/agents/01-core-development/graphql-architect.md +238 -0
  10. package/.claude/agents/01-core-development/microservices-architect.md +239 -0
  11. package/.claude/agents/01-core-development/mobile-developer.md +283 -0
  12. package/.claude/agents/01-core-development/ui-designer.md +174 -0
  13. package/.claude/agents/01-core-development/websocket-engineer.md +150 -0
  14. package/.claude/agents/03-infrastructure/azure-infra-engineer.md +53 -0
  15. package/.claude/agents/03-infrastructure/cloud-architect.md +277 -0
  16. package/.claude/agents/03-infrastructure/database-administrator.md +287 -0
  17. package/.claude/agents/03-infrastructure/deployment-engineer.md +287 -0
  18. package/.claude/agents/03-infrastructure/devops-engineer.md +287 -0
  19. package/.claude/agents/03-infrastructure/devops-incident-responder.md +287 -0
  20. package/.claude/agents/03-infrastructure/docker-expert.md +278 -0
  21. package/.claude/agents/03-infrastructure/incident-responder.md +287 -0
  22. package/.claude/agents/03-infrastructure/kubernetes-specialist.md +287 -0
  23. package/.claude/agents/03-infrastructure/network-engineer.md +287 -0
  24. package/.claude/agents/03-infrastructure/platform-engineer.md +287 -0
  25. package/.claude/agents/03-infrastructure/security-engineer.md +277 -0
  26. package/.claude/agents/03-infrastructure/sre-engineer.md +287 -0
  27. package/.claude/agents/03-infrastructure/terraform-engineer.md +287 -0
  28. package/.claude/agents/03-infrastructure/terragrunt-expert.md +307 -0
  29. package/.claude/agents/03-infrastructure/windows-infra-admin.md +52 -0
  30. package/.claude/agents/04-quality-security/accessibility-tester.md +277 -0
  31. package/.claude/agents/04-quality-security/ad-security-reviewer.md +56 -0
  32. package/.claude/agents/04-quality-security/ai-writing-auditor.md +77 -0
  33. package/.claude/agents/04-quality-security/architect-reviewer.md +287 -0
  34. package/.claude/agents/04-quality-security/chaos-engineer.md +277 -0
  35. package/.claude/agents/04-quality-security/code-reviewer.md +287 -0
  36. package/.claude/agents/04-quality-security/compliance-auditor.md +277 -0
  37. package/.claude/agents/04-quality-security/debugger.md +287 -0
  38. package/.claude/agents/04-quality-security/error-detective.md +287 -0
  39. package/.claude/agents/04-quality-security/penetration-tester.md +287 -0
  40. package/.claude/agents/04-quality-security/performance-engineer.md +287 -0
  41. package/.claude/agents/04-quality-security/powershell-security-hardening.md +54 -0
  42. package/.claude/agents/04-quality-security/qa-expert.md +287 -0
  43. package/.claude/agents/04-quality-security/security-auditor.md +287 -0
  44. package/.claude/agents/04-quality-security/test-automator.md +287 -0
  45. package/.claude/agents/04-quality-security/ui-ux-tester.md +234 -0
  46. package/.claude/agents/06-developer-experience/build-engineer.md +286 -0
  47. package/.claude/agents/06-developer-experience/cli-developer.md +286 -0
  48. package/.claude/agents/06-developer-experience/dependency-manager.md +286 -0
  49. package/.claude/agents/06-developer-experience/documentation-engineer.md +276 -0
  50. package/.claude/agents/06-developer-experience/dx-optimizer.md +286 -0
  51. package/.claude/agents/06-developer-experience/git-workflow-manager.md +286 -0
  52. package/.claude/agents/06-developer-experience/legacy-modernizer.md +286 -0
  53. package/.claude/agents/06-developer-experience/mcp-developer.md +275 -0
  54. package/.claude/agents/06-developer-experience/powershell-module-architect.md +58 -0
  55. package/.claude/agents/06-developer-experience/powershell-ui-architect.md +135 -0
  56. package/.claude/agents/06-developer-experience/readme-generator.md +238 -0
  57. package/.claude/agents/06-developer-experience/refactoring-specialist.md +286 -0
  58. package/.claude/agents/06-developer-experience/slack-expert.md +232 -0
  59. package/.claude/agents/06-developer-experience/tooling-engineer.md +286 -0
  60. package/.claude/agents/09-meta-orchestration/agent-installer.md +97 -0
  61. package/.claude/agents/09-meta-orchestration/agent-organizer.md +287 -0
  62. package/.claude/agents/09-meta-orchestration/codebase-orchestrator.md +249 -0
  63. package/.claude/agents/09-meta-orchestration/context-manager.md +287 -0
  64. package/.claude/agents/09-meta-orchestration/error-coordinator.md +287 -0
  65. package/.claude/agents/09-meta-orchestration/it-ops-orchestrator.md +60 -0
  66. package/.claude/agents/09-meta-orchestration/knowledge-synthesizer.md +287 -0
  67. package/.claude/agents/09-meta-orchestration/multi-agent-coordinator.md +287 -0
  68. package/.claude/agents/09-meta-orchestration/performance-monitor.md +287 -0
  69. package/.claude/agents/09-meta-orchestration/task-distributor.md +287 -0
  70. package/.claude/agents/09-meta-orchestration/workflow-orchestrator.md +287 -0
  71. package/.claude/agents/10-research-analysis/competitive-analyst.md +287 -0
  72. package/.claude/agents/10-research-analysis/data-researcher.md +287 -0
  73. package/.claude/agents/10-research-analysis/market-researcher.md +287 -0
  74. package/.claude/agents/10-research-analysis/project-idea-validator.md +269 -0
  75. package/.claude/agents/10-research-analysis/research-analyst.md +287 -0
  76. package/.claude/agents/10-research-analysis/scientific-literature-researcher.md +151 -0
  77. package/.claude/agents/10-research-analysis/search-specialist.md +287 -0
  78. package/.claude/agents/10-research-analysis/trend-analyst.md +287 -0
  79. package/.claude/archive/README.md +47 -0
  80. package/.claude/archive/agents/02-language-specialists/angular-architect.md +287 -0
  81. package/.claude/archive/agents/02-language-specialists/cpp-pro.md +277 -0
  82. package/.claude/archive/agents/02-language-specialists/csharp-developer.md +287 -0
  83. package/.claude/archive/agents/02-language-specialists/django-developer.md +287 -0
  84. package/.claude/archive/agents/02-language-specialists/dotnet-core-expert.md +287 -0
  85. package/.claude/archive/agents/02-language-specialists/dotnet-framework-4.8-expert.md +306 -0
  86. package/.claude/archive/agents/02-language-specialists/elixir-expert.md +311 -0
  87. package/.claude/archive/agents/02-language-specialists/expo-react-native-expert.md +268 -0
  88. package/.claude/archive/agents/02-language-specialists/fastapi-developer.md +287 -0
  89. package/.claude/archive/agents/02-language-specialists/flutter-expert.md +287 -0
  90. package/.claude/archive/agents/02-language-specialists/golang-pro.md +277 -0
  91. package/.claude/archive/agents/02-language-specialists/java-architect.md +287 -0
  92. package/.claude/archive/agents/02-language-specialists/javascript-pro.md +277 -0
  93. package/.claude/archive/agents/02-language-specialists/kotlin-specialist.md +287 -0
  94. package/.claude/archive/agents/02-language-specialists/laravel-specialist.md +287 -0
  95. package/.claude/archive/agents/02-language-specialists/nextjs-developer.md +287 -0
  96. package/.claude/archive/agents/02-language-specialists/node-specialist.md +124 -0
  97. package/.claude/archive/agents/02-language-specialists/php-pro.md +287 -0
  98. package/.claude/archive/agents/02-language-specialists/powershell-5.1-expert.md +59 -0
  99. package/.claude/archive/agents/02-language-specialists/powershell-7-expert.md +57 -0
  100. package/.claude/archive/agents/02-language-specialists/python-pro.md +277 -0
  101. package/.claude/archive/agents/02-language-specialists/rails-expert.md +358 -0
  102. package/.claude/archive/agents/02-language-specialists/react-specialist.md +287 -0
  103. package/.claude/archive/agents/02-language-specialists/rust-engineer.md +287 -0
  104. package/.claude/archive/agents/02-language-specialists/spring-boot-engineer.md +287 -0
  105. package/.claude/archive/agents/02-language-specialists/sql-pro.md +287 -0
  106. package/.claude/archive/agents/02-language-specialists/swift-expert.md +287 -0
  107. package/.claude/archive/agents/02-language-specialists/symfony-specialist.md +354 -0
  108. package/.claude/archive/agents/02-language-specialists/typescript-pro.md +277 -0
  109. package/.claude/archive/agents/02-language-specialists/vue-expert.md +287 -0
  110. package/.claude/archive/agents/05-data-ai/ai-engineer.md +287 -0
  111. package/.claude/archive/agents/05-data-ai/data-analyst.md +277 -0
  112. package/.claude/archive/agents/05-data-ai/data-engineer.md +287 -0
  113. package/.claude/archive/agents/05-data-ai/data-scientist.md +287 -0
  114. package/.claude/archive/agents/05-data-ai/database-optimizer.md +287 -0
  115. package/.claude/archive/agents/05-data-ai/llm-architect.md +287 -0
  116. package/.claude/archive/agents/05-data-ai/machine-learning-engineer.md +277 -0
  117. package/.claude/archive/agents/05-data-ai/ml-engineer.md +287 -0
  118. package/.claude/archive/agents/05-data-ai/mlops-engineer.md +287 -0
  119. package/.claude/archive/agents/05-data-ai/nlp-engineer.md +287 -0
  120. package/.claude/archive/agents/05-data-ai/postgres-pro.md +287 -0
  121. package/.claude/archive/agents/05-data-ai/prompt-engineer.md +287 -0
  122. package/.claude/archive/agents/05-data-ai/reinforcement-learning-engineer.md +277 -0
  123. package/.claude/archive/agents/07-specialized-domains/api-documenter.md +277 -0
  124. package/.claude/archive/agents/07-specialized-domains/blockchain-developer.md +287 -0
  125. package/.claude/archive/agents/07-specialized-domains/embedded-systems.md +287 -0
  126. package/.claude/archive/agents/07-specialized-domains/fintech-engineer.md +287 -0
  127. package/.claude/archive/agents/07-specialized-domains/game-developer.md +287 -0
  128. package/.claude/archive/agents/07-specialized-domains/healthcare-admin.md +199 -0
  129. package/.claude/archive/agents/07-specialized-domains/iot-engineer.md +287 -0
  130. package/.claude/archive/agents/07-specialized-domains/m365-admin.md +48 -0
  131. package/.claude/archive/agents/07-specialized-domains/mobile-app-developer.md +287 -0
  132. package/.claude/archive/agents/07-specialized-domains/payment-integration.md +287 -0
  133. package/.claude/archive/agents/07-specialized-domains/quant-analyst.md +287 -0
  134. package/.claude/archive/agents/07-specialized-domains/risk-manager.md +287 -0
  135. package/.claude/archive/agents/07-specialized-domains/seo-specialist.md +184 -0
  136. package/.claude/archive/agents/08-business-product/business-analyst.md +287 -0
  137. package/.claude/archive/agents/08-business-product/content-marketer.md +287 -0
  138. package/.claude/archive/agents/08-business-product/customer-success-manager.md +287 -0
  139. package/.claude/archive/agents/08-business-product/legal-advisor.md +287 -0
  140. package/.claude/archive/agents/08-business-product/license-engineer.md +295 -0
  141. package/.claude/archive/agents/08-business-product/product-manager.md +287 -0
  142. package/.claude/archive/agents/08-business-product/project-manager.md +287 -0
  143. package/.claude/archive/agents/08-business-product/sales-engineer.md +287 -0
  144. package/.claude/archive/agents/08-business-product/scrum-master.md +287 -0
  145. package/.claude/archive/agents/08-business-product/technical-writer.md +287 -0
  146. package/.claude/archive/agents/08-business-product/ux-researcher.md +287 -0
  147. package/.claude/archive/agents/08-business-product/wordpress-master.md +316 -0
  148. package/.claude/archive/skills/competitive-ads-extractor/SKILL.md +293 -0
  149. package/.claude/archive/skills/developer-growth-analysis/SKILL.md +322 -0
  150. package/.claude/archive/skills/document-docx/LICENSE.txt +30 -0
  151. package/.claude/archive/skills/document-docx/SKILL.md +197 -0
  152. package/.claude/archive/skills/document-docx/docx-js.md +350 -0
  153. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  154. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  155. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  156. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  157. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  158. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  159. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  160. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  161. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  162. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  163. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  164. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  165. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  166. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  167. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  168. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  169. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  170. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  171. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  172. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  173. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  174. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  175. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  176. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  177. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  178. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  179. package/.claude/archive/skills/document-docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  180. package/.claude/archive/skills/document-docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  181. package/.claude/archive/skills/document-docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  182. package/.claude/archive/skills/document-docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  183. package/.claude/archive/skills/document-docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  184. package/.claude/archive/skills/document-docx/ooxml/schemas/mce/mc.xsd +75 -0
  185. package/.claude/archive/skills/document-docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  186. package/.claude/archive/skills/document-docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  187. package/.claude/archive/skills/document-docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  188. package/.claude/archive/skills/document-docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  189. package/.claude/archive/skills/document-docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  190. package/.claude/archive/skills/document-docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  191. package/.claude/archive/skills/document-docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  192. package/.claude/archive/skills/document-docx/ooxml/scripts/pack.py +159 -0
  193. package/.claude/archive/skills/document-docx/ooxml/scripts/unpack.py +29 -0
  194. package/.claude/archive/skills/document-docx/ooxml/scripts/validate.py +69 -0
  195. package/.claude/archive/skills/document-docx/ooxml/scripts/validation/__init__.py +15 -0
  196. package/.claude/archive/skills/document-docx/ooxml/scripts/validation/base.py +951 -0
  197. package/.claude/archive/skills/document-docx/ooxml/scripts/validation/docx.py +274 -0
  198. package/.claude/archive/skills/document-docx/ooxml/scripts/validation/pptx.py +315 -0
  199. package/.claude/archive/skills/document-docx/ooxml/scripts/validation/redlining.py +279 -0
  200. package/.claude/archive/skills/document-docx/ooxml.md +610 -0
  201. package/.claude/archive/skills/document-docx/scripts/__init__.py +1 -0
  202. package/.claude/archive/skills/document-docx/scripts/document.py +1276 -0
  203. package/.claude/archive/skills/document-docx/scripts/templates/comments.xml +3 -0
  204. package/.claude/archive/skills/document-docx/scripts/templates/commentsExtended.xml +3 -0
  205. package/.claude/archive/skills/document-docx/scripts/templates/commentsExtensible.xml +3 -0
  206. package/.claude/archive/skills/document-docx/scripts/templates/commentsIds.xml +3 -0
  207. package/.claude/archive/skills/document-docx/scripts/templates/people.xml +3 -0
  208. package/.claude/archive/skills/document-docx/scripts/utilities.py +374 -0
  209. package/.claude/archive/skills/document-pdf/LICENSE.txt +30 -0
  210. package/.claude/archive/skills/document-pdf/SKILL.md +294 -0
  211. package/.claude/archive/skills/document-pdf/forms.md +205 -0
  212. package/.claude/archive/skills/document-pdf/reference.md +612 -0
  213. package/.claude/archive/skills/document-pdf/scripts/check_bounding_boxes.py +70 -0
  214. package/.claude/archive/skills/document-pdf/scripts/check_bounding_boxes_test.py +226 -0
  215. package/.claude/archive/skills/document-pdf/scripts/check_fillable_fields.py +12 -0
  216. package/.claude/archive/skills/document-pdf/scripts/convert_pdf_to_images.py +35 -0
  217. package/.claude/archive/skills/document-pdf/scripts/create_validation_image.py +41 -0
  218. package/.claude/archive/skills/document-pdf/scripts/extract_form_field_info.py +152 -0
  219. package/.claude/archive/skills/document-pdf/scripts/fill_fillable_fields.py +114 -0
  220. package/.claude/archive/skills/document-pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
  221. package/.claude/archive/skills/document-pptx/LICENSE.txt +30 -0
  222. package/.claude/archive/skills/document-pptx/SKILL.md +484 -0
  223. package/.claude/archive/skills/document-pptx/html2pptx.md +625 -0
  224. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  225. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  226. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  227. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  228. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  229. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  230. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  231. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  232. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  233. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  234. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  235. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  236. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  237. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  238. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  239. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  240. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  241. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  242. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  243. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  244. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  245. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  246. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  247. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  248. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  249. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  250. package/.claude/archive/skills/document-pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  251. package/.claude/archive/skills/document-pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  252. package/.claude/archive/skills/document-pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  253. package/.claude/archive/skills/document-pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  254. package/.claude/archive/skills/document-pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  255. package/.claude/archive/skills/document-pptx/ooxml/schemas/mce/mc.xsd +75 -0
  256. package/.claude/archive/skills/document-pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  257. package/.claude/archive/skills/document-pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  258. package/.claude/archive/skills/document-pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  259. package/.claude/archive/skills/document-pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  260. package/.claude/archive/skills/document-pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  261. package/.claude/archive/skills/document-pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  262. package/.claude/archive/skills/document-pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  263. package/.claude/archive/skills/document-pptx/ooxml/scripts/pack.py +159 -0
  264. package/.claude/archive/skills/document-pptx/ooxml/scripts/unpack.py +29 -0
  265. package/.claude/archive/skills/document-pptx/ooxml/scripts/validate.py +69 -0
  266. package/.claude/archive/skills/document-pptx/ooxml/scripts/validation/__init__.py +15 -0
  267. package/.claude/archive/skills/document-pptx/ooxml/scripts/validation/base.py +951 -0
  268. package/.claude/archive/skills/document-pptx/ooxml/scripts/validation/docx.py +274 -0
  269. package/.claude/archive/skills/document-pptx/ooxml/scripts/validation/pptx.py +315 -0
  270. package/.claude/archive/skills/document-pptx/ooxml/scripts/validation/redlining.py +279 -0
  271. package/.claude/archive/skills/document-pptx/ooxml.md +427 -0
  272. package/.claude/archive/skills/document-pptx/scripts/html2pptx.js +979 -0
  273. package/.claude/archive/skills/document-pptx/scripts/inventory.py +1020 -0
  274. package/.claude/archive/skills/document-pptx/scripts/rearrange.py +231 -0
  275. package/.claude/archive/skills/document-pptx/scripts/replace.py +385 -0
  276. package/.claude/archive/skills/document-pptx/scripts/thumbnail.py +450 -0
  277. package/.claude/archive/skills/document-xlsx/LICENSE.txt +30 -0
  278. package/.claude/archive/skills/document-xlsx/SKILL.md +289 -0
  279. package/.claude/archive/skills/document-xlsx/recalc.py +178 -0
  280. package/.claude/archive/skills/image-enhancer/SKILL.md +99 -0
  281. package/.claude/archive/skills/meeting-insights-analyzer/SKILL.md +327 -0
  282. package/.claude/archive/skills/slack-gif-creator/LICENSE.txt +202 -0
  283. package/.claude/archive/skills/slack-gif-creator/SKILL.md +646 -0
  284. package/.claude/archive/skills/slack-gif-creator/core/color_palettes.py +302 -0
  285. package/.claude/archive/skills/slack-gif-creator/core/easing.py +230 -0
  286. package/.claude/archive/skills/slack-gif-creator/core/frame_composer.py +469 -0
  287. package/.claude/archive/skills/slack-gif-creator/core/gif_builder.py +246 -0
  288. package/.claude/archive/skills/slack-gif-creator/core/typography.py +357 -0
  289. package/.claude/archive/skills/slack-gif-creator/core/validators.py +264 -0
  290. package/.claude/archive/skills/slack-gif-creator/core/visual_effects.py +494 -0
  291. package/.claude/archive/skills/slack-gif-creator/requirements.txt +4 -0
  292. package/.claude/archive/skills/slack-gif-creator/templates/bounce.py +106 -0
  293. package/.claude/archive/skills/slack-gif-creator/templates/explode.py +331 -0
  294. package/.claude/archive/skills/slack-gif-creator/templates/fade.py +329 -0
  295. package/.claude/archive/skills/slack-gif-creator/templates/flip.py +291 -0
  296. package/.claude/archive/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
  297. package/.claude/archive/skills/slack-gif-creator/templates/morph.py +329 -0
  298. package/.claude/archive/skills/slack-gif-creator/templates/move.py +293 -0
  299. package/.claude/archive/skills/slack-gif-creator/templates/pulse.py +268 -0
  300. package/.claude/archive/skills/slack-gif-creator/templates/shake.py +127 -0
  301. package/.claude/archive/skills/slack-gif-creator/templates/slide.py +291 -0
  302. package/.claude/archive/skills/slack-gif-creator/templates/spin.py +269 -0
  303. package/.claude/archive/skills/slack-gif-creator/templates/wiggle.py +300 -0
  304. package/.claude/archive/skills/slack-gif-creator/templates/zoom.py +312 -0
  305. package/.claude/archive/skills/twitter-algorithm-optimizer/SKILL.md +327 -0
  306. package/.claude/archive/skills/video-downloader/SKILL.md +99 -0
  307. package/.claude/archive/skills/video-downloader/scripts/download_video.py +145 -0
  308. package/.claude/bash-whitelist-requests/2026-05-28-grep-find-rg.md +68 -0
  309. package/.claude/bash-whitelist-requests/2026-06-01-readonly-filters.md +76 -0
  310. package/.claude/bash-whitelist.txt +124 -0
  311. package/.claude/commands/agent-introspect.md +89 -0
  312. package/.claude/commands/apply-rules.md +363 -0
  313. package/.claude/commands/approve-design.md +219 -0
  314. package/.claude/commands/approve-org-money.md +267 -0
  315. package/.claude/commands/build.md +234 -0
  316. package/.claude/commands/commit.md +97 -0
  317. package/.claude/commands/context-fetch.md +113 -0
  318. package/.claude/commands/create-tool.md +496 -0
  319. package/.claude/commands/design-review.md +138 -0
  320. package/.claude/commands/design.md +807 -0
  321. package/.claude/commands/discharge-byproduct.md +208 -0
  322. package/.claude/commands/doc-review.md +165 -0
  323. package/.claude/commands/document-pair.md +76 -0
  324. package/.claude/commands/error-triage.md +435 -0
  325. package/.claude/commands/eval.md +70 -0
  326. package/.claude/commands/evolve.md +49 -0
  327. package/.claude/commands/finish-task.md +105 -0
  328. package/.claude/commands/gan-build.md +91 -0
  329. package/.claude/commands/gan-design.md +82 -0
  330. package/.claude/commands/gate-bypass.md +77 -0
  331. package/.claude/commands/gate-clear.md +45 -0
  332. package/.claude/commands/gate-status.md +46 -0
  333. package/.claude/commands/harness-audit.md +151 -0
  334. package/.claude/commands/hearing.md +138 -0
  335. package/.claude/commands/impact-check.md +486 -0
  336. package/.claude/commands/init-tasks.md +49 -0
  337. package/.claude/commands/instinct-export.md +47 -0
  338. package/.claude/commands/instinct-import.md +41 -0
  339. package/.claude/commands/instinct-status.md +43 -0
  340. package/.claude/commands/investigate.md +547 -0
  341. package/.claude/commands/learn.md +55 -0
  342. package/.claude/commands/lint-rules.md +400 -0
  343. package/.claude/commands/mode.md +58 -0
  344. package/.claude/commands/modify-feature.md +209 -0
  345. package/.claude/commands/module-review.md +149 -0
  346. package/.claude/commands/move-section.md +67 -0
  347. package/.claude/commands/new-draft.md +67 -0
  348. package/.claude/commands/new-feature.md +286 -0
  349. package/.claude/commands/new-task.md +156 -0
  350. package/.claude/commands/notification.md +107 -0
  351. package/.claude/commands/pm-start.md +119 -0
  352. package/.claude/commands/projects.md +32 -0
  353. package/.claude/commands/promote.md +43 -0
  354. package/.claude/commands/rasis-report.md +1323 -0
  355. package/.claude/commands/release-note.md +130 -0
  356. package/.claude/commands/reply-watch.md +149 -0
  357. package/.claude/commands/requirement.md +352 -0
  358. package/.claude/commands/resume-state.md +187 -0
  359. package/.claude/commands/reviewpr.md +118 -0
  360. package/.claude/commands/save-state.md +100 -0
  361. package/.claude/commands/sentry-pr.md +157 -0
  362. package/.claude/commands/start-task.md +87 -0
  363. package/.claude/commands/system-review.md +147 -0
  364. package/.claude/commands/task-bypass.md +70 -0
  365. package/.claude/commands/task-estimate.md +100 -0
  366. package/.claude/commands/template-apply.md +89 -0
  367. package/.claude/commands/test-design.md +116 -0
  368. package/.claude/commands/transfer-mismatch.md +317 -0
  369. package/.claude/commands/verify.md +51 -0
  370. package/.claude/evals/grader-loop-mode-autonomy.sh +165 -0
  371. package/.claude/evals/grader-system-reminder-attention.sh +99 -0
  372. package/.claude/evals/loop-mode-autonomy.md +121 -0
  373. package/.claude/evals/loop-mode-autonomy.results.template.md +133 -0
  374. package/.claude/evals/system-reminder-attention.md +123 -0
  375. package/.claude/evals/system-reminder-attention.results.template.md +93 -0
  376. package/.claude/evals/system-reminder-attention.runner.md +353 -0
  377. package/.claude/harness-config.local.yml +48 -0
  378. package/.claude/harness-config.yml +534 -0
  379. package/.claude/hooks/agent-marker-clear.sh +43 -0
  380. package/.claude/hooks/agent-marker-set.sh +40 -0
  381. package/.claude/hooks/agent-router-suggest.sh +123 -0
  382. package/.claude/hooks/autonomous-action-guard.sh +242 -0
  383. package/.claude/hooks/byproduct-discharge-guard.sh +128 -0
  384. package/.claude/hooks/check-md-mermaid.sh +144 -0
  385. package/.claude/hooks/check-required-env.sh +95 -0
  386. package/.claude/hooks/check-serena-mcp.sh +123 -0
  387. package/.claude/hooks/confidence-gate.sh +139 -0
  388. package/.claude/hooks/context-budget.sh +233 -0
  389. package/.claude/hooks/delegation-guard.sh +99 -0
  390. package/.claude/hooks/dispatcher-manifest.tsv +38 -0
  391. package/.claude/hooks/draft-flow-guard.sh +304 -0
  392. package/.claude/hooks/failure-loop-detect.sh +139 -0
  393. package/.claude/hooks/gateguard.sh +209 -0
  394. package/.claude/hooks/improvement-proposal.sh +112 -0
  395. package/.claude/hooks/init-tasks-on-start.sh +34 -0
  396. package/.claude/hooks/lib/bypass-logger.sh +82 -0
  397. package/.claude/hooks/lib/confidence-gate/bypass.sh +48 -0
  398. package/.claude/hooks/lib/confidence-gate/extract.sh +99 -0
  399. package/.claude/hooks/lib/confidence-gate/major-agent-filter.sh +59 -0
  400. package/.claude/hooks/lib/confidence-gate/messages.sh +53 -0
  401. package/.claude/hooks/lib/config-loader.sh +784 -0
  402. package/.claude/hooks/lib/delegation-guard/bash-whitelist.sh +323 -0
  403. package/.claude/hooks/lib/delegation-guard/git-deny.sh +188 -0
  404. package/.claude/hooks/lib/delegation-guard/protected-paths.sh +105 -0
  405. package/.claude/hooks/lib/delegation-guard/subagent-detect.sh +40 -0
  406. package/.claude/hooks/lib/dispatcher-core.sh +454 -0
  407. package/.claude/hooks/lib/improvement-proposal/aggregate.py +466 -0
  408. package/.claude/hooks/lib/improvement-proposal/cache.sh +78 -0
  409. package/.claude/hooks/lib/mode-loader.sh +80 -0
  410. package/.claude/hooks/lib/next-actions-parser.sh +153 -0
  411. package/.claude/hooks/lib/project-root.sh +60 -0
  412. package/.claude/hooks/list-md-plan-first-reminder.sh +143 -0
  413. package/.claude/hooks/loop-auto-progress-reminder.sh +108 -0
  414. package/.claude/hooks/loop-confirmation-detector.sh +241 -0
  415. package/.claude/hooks/mode-asana-prompt.sh +61 -0
  416. package/.claude/hooks/mode-enforce.sh +57 -0
  417. package/.claude/hooks/mode-session-start.sh +93 -0
  418. package/.claude/hooks/next-actions-surface.sh +136 -0
  419. package/.claude/hooks/notification-dispatcher.sh +9 -0
  420. package/.claude/hooks/notify.sh +27 -0
  421. package/.claude/hooks/parallel-subagent-reminder.sh +469 -0
  422. package/.claude/hooks/post-tool-use-dispatcher.sh +9 -0
  423. package/.claude/hooks/pre-tool-use-dispatcher.sh +9 -0
  424. package/.claude/hooks/reviewer-count-guard.sh +313 -0
  425. package/.claude/hooks/session-help-surface.sh +192 -0
  426. package/.claude/hooks/session-start-dispatcher.sh +9 -0
  427. package/.claude/hooks/session-start-wrapper.sh +156 -0
  428. package/.claude/hooks/stale-harness-detect.sh +422 -0
  429. package/.claude/hooks/stop-dispatcher.sh +9 -0
  430. package/.claude/hooks/stop.sh +25 -0
  431. package/.claude/hooks/subagent-stop-dispatcher.sh +9 -0
  432. package/.claude/hooks/task-rule-guard.sh +317 -0
  433. package/.claude/hooks/tests/run-tests.sh +23 -0
  434. package/.claude/hooks/tests/test-agent-marker-warn.sh +86 -0
  435. package/.claude/hooks/tests/test-check-required-env.sh +138 -0
  436. package/.claude/hooks/tests/test-confidence-gate.sh +170 -0
  437. package/.claude/hooks/tests/test-config-env-override.sh +220 -0
  438. package/.claude/hooks/tests/test-gate-disable.sh +118 -0
  439. package/.claude/hooks/tests/test-improvement-proposal.sh +284 -0
  440. package/.claude/hooks/tool-call-slip-detector.sh +188 -0
  441. package/.claude/hooks/user-prompt-submit-dispatcher.sh +9 -0
  442. package/.claude/hooks/why-x5-reminder.sh +45 -0
  443. package/.claude/hooks/why-x5-violation-detect.sh +152 -0
  444. package/.claude/hooks/workflow-guard.sh +263 -0
  445. package/.claude/mode.yml +28 -0
  446. package/.claude/project-rules/development-process.md +8 -0
  447. package/.claude/project-rules/git-workflow.md +8 -0
  448. package/.claude/project-rules/modes.md +8 -0
  449. package/.claude/project-rules/self-improvement.md +8 -0
  450. package/.claude/project-rules/task-management.md +8 -0
  451. package/.claude/project-rules/why-x5-output.md +8 -0
  452. package/.claude/project-rules/workflow.md +8 -0
  453. package/.claude/rules/development-process.md +293 -0
  454. package/.claude/rules/git-workflow.md +71 -0
  455. package/.claude/rules/modes.md +189 -0
  456. package/.claude/rules/self-improvement.md +76 -0
  457. package/.claude/rules/task-management.md +261 -0
  458. package/.claude/rules/why-x5-output.md +97 -0
  459. package/.claude/rules/workflow.md +157 -0
  460. package/.claude/rules-details/README.md +67 -0
  461. package/.claude/rules-details/development-process/confidence-gate.md +22 -0
  462. package/.claude/rules-details/development-process/cross-repo-write.md +35 -0
  463. package/.claude/rules-details/development-process/delegation-requirements.md +158 -0
  464. package/.claude/rules-details/development-process/harness-sync.md +21 -0
  465. package/.claude/rules-details/development-process/origin.md +13 -0
  466. package/.claude/rules-details/development-process/parallelization-origin.md +22 -0
  467. package/.claude/rules-details/development-process/research-reuse.md +22 -0
  468. package/.claude/rules-details/development-process/staging-strategy.md +47 -0
  469. package/.claude/rules-details/modes/artifacts.md +34 -0
  470. package/.claude/rules-details/modes/compliance-items.md +120 -0
  471. package/.claude/rules-details/modes/five-layer-enforcement.md +46 -0
  472. package/.claude/rules-details/modes/mode-hooks.md +51 -0
  473. package/.claude/rules-details/modes/origin.md +17 -0
  474. package/.claude/rules-details/self-improvement/l4-mechanics.md +36 -0
  475. package/.claude/rules-details/self-improvement/origin.md +8 -0
  476. package/.claude/rules-details/self-improvement/related-skills.md +35 -0
  477. package/.claude/rules-details/self-improvement/when-to-use-layers.md +39 -0
  478. package/.claude/rules-details/task-management/hook-enforcement.md +25 -0
  479. package/.claude/rules-details/task-management/mandatory-reading.md +20 -0
  480. package/.claude/rules-details/task-management/origin.md +12 -0
  481. package/.claude/rules-details/task-management/parking-lot.md +26 -0
  482. package/.claude/rules-details/task-management/plan-first.md +44 -0
  483. package/.claude/rules-details/task-management/six-articles.md +68 -0
  484. package/.claude/rules-details/task-management/task-migration.md +16 -0
  485. package/.claude/rules-details/task-management/ui-detection.md +11 -0
  486. package/.claude/rules-details/why-x5-output/examples.md +41 -0
  487. package/.claude/rules-details/why-x5-output/feedback-memory.md +14 -0
  488. package/.claude/rules-details/why-x5-output/origin.md +10 -0
  489. package/.claude/rules-details/why-x5-output/v1-v10-history.md +19 -0
  490. package/.claude/rules-details/workflow/10-stage.md +43 -0
  491. package/.claude/rules-details/workflow/14-stage.md +52 -0
  492. package/.claude/rules-details/workflow/byproduct-discharge.md +39 -0
  493. package/.claude/rules-details/workflow/draft-flow-guard.md +31 -0
  494. package/.claude/rules-details/workflow/fan-out.md +70 -0
  495. package/.claude/rules-details/workflow/mece-20.md +36 -0
  496. package/.claude/rules-details/workflow/origin.md +14 -0
  497. package/.claude/rules-details/workflow/refactoring.md +48 -0
  498. package/.claude/rules-details/workflow/related-skills.md +22 -0
  499. package/.claude/rules-details/workflow/reviewer-prompt.md +100 -0
  500. package/.claude/rules-details/workflow/session-persistence.md +46 -0
  501. package/.claude/rules-details/workflow/workflow-guard.md +36 -0
  502. package/.claude/scripts/__pycache__/harness-audit.cpython-313.pyc +0 -0
  503. package/.claude/scripts/agent-stocktake.py +421 -0
  504. package/.claude/scripts/check-md-mermaid.mjs +138 -0
  505. package/.claude/scripts/generate-settings.sh +0 -0
  506. package/.claude/scripts/harness-audit.py +1547 -0
  507. package/.claude/scripts/hc-config.sh +2265 -0
  508. package/.claude/scripts/init-tasks.sh +117 -0
  509. package/.claude/scripts/lib/enforcement-matrix-parse.sh +81 -0
  510. package/.claude/scripts/lib/hc-config-metadata.sh +190 -0
  511. package/.claude/scripts/lib/hc-config-web-server.js +1528 -0
  512. package/.claude/scripts/lib/hc-config-web-ui/app.js +1054 -0
  513. package/.claude/scripts/lib/hc-config-web-ui/index.html +130 -0
  514. package/.claude/scripts/lib/hc-config-web-ui/style.css +522 -0
  515. package/.claude/scripts/new-task-helper.sh +432 -0
  516. package/.claude/scripts/observe-repair.sh +437 -0
  517. package/.claude/scripts/observe-rotate.sh +311 -0
  518. package/.claude/scripts/statusline.sh +239 -0
  519. package/.claude/settings.generated.preview.json +211 -0
  520. package/.claude/settings.json +215 -0
  521. package/.claude/settings.local.example.json +20 -0
  522. package/.claude/settings.local.json +36 -0
  523. package/.claude/skills/agent-introspection-debugging/SKILL.md +123 -0
  524. package/.claude/skills/agent-router/README.md +137 -0
  525. package/.claude/skills/agent-router/SKILL.md +74 -0
  526. package/.claude/skills/agent-router/dispatch-table.yml +352 -0
  527. package/.claude/skills/agent-router/router.py +1086 -0
  528. package/.claude/skills/agent-router/samples/representative_prompts.txt +24 -0
  529. package/.claude/skills/agent-router/tests/__init__.py +0 -0
  530. package/.claude/skills/agent-router/tests/test_router.py +762 -0
  531. package/.claude/skills/artifacts-builder/LICENSE.txt +202 -0
  532. package/.claude/skills/artifacts-builder/SKILL.md +74 -0
  533. package/.claude/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  534. package/.claude/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
  535. package/.claude/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  536. package/.claude/skills/brand-guidelines/LICENSE.txt +202 -0
  537. package/.claude/skills/brand-guidelines/SKILL.md +73 -0
  538. package/.claude/skills/canvas-design/LICENSE.txt +202 -0
  539. package/.claude/skills/canvas-design/SKILL.md +130 -0
  540. package/.claude/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  541. package/.claude/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  542. package/.claude/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  543. package/.claude/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +93 -0
  544. package/.claude/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  545. package/.claude/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +93 -0
  546. package/.claude/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
  547. package/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  548. package/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  549. package/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  550. package/.claude/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  551. package/.claude/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  552. package/.claude/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  553. package/.claude/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  554. package/.claude/skills/canvas-design/canvas-fonts/DMMono-OFL.txt +93 -0
  555. package/.claude/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
  556. package/.claude/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +94 -0
  557. package/.claude/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
  558. package/.claude/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
  559. package/.claude/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +93 -0
  560. package/.claude/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
  561. package/.claude/skills/canvas-design/canvas-fonts/Gloock-OFL.txt +93 -0
  562. package/.claude/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
  563. package/.claude/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  564. package/.claude/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  565. package/.claude/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  566. package/.claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  567. package/.claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  568. package/.claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  569. package/.claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  570. package/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  571. package/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  572. package/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  573. package/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  574. package/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  575. package/.claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  576. package/.claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  577. package/.claude/skills/canvas-design/canvas-fonts/Italiana-OFL.txt +93 -0
  578. package/.claude/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
  579. package/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  580. package/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  581. package/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  582. package/.claude/skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
  583. package/.claude/skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
  584. package/.claude/skills/canvas-design/canvas-fonts/Jura-OFL.txt +93 -0
  585. package/.claude/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  586. package/.claude/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  587. package/.claude/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
  588. package/.claude/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  589. package/.claude/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
  590. package/.claude/skills/canvas-design/canvas-fonts/Lora-OFL.txt +93 -0
  591. package/.claude/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
  592. package/.claude/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
  593. package/.claude/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +93 -0
  594. package/.claude/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
  595. package/.claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  596. package/.claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  597. package/.claude/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
  598. package/.claude/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +93 -0
  599. package/.claude/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
  600. package/.claude/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  601. package/.claude/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +93 -0
  602. package/.claude/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +93 -0
  603. package/.claude/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  604. package/.claude/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  605. package/.claude/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +93 -0
  606. package/.claude/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  607. package/.claude/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +93 -0
  608. package/.claude/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  609. package/.claude/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  610. package/.claude/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +93 -0
  611. package/.claude/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
  612. package/.claude/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +93 -0
  613. package/.claude/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
  614. package/.claude/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
  615. package/.claude/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  616. package/.claude/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
  617. package/.claude/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +93 -0
  618. package/.claude/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
  619. package/.claude/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +93 -0
  620. package/.claude/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  621. package/.claude/skills/changelog-generator/SKILL.md +104 -0
  622. package/.claude/skills/check-md-mermaid/SKILL.md +62 -0
  623. package/.claude/skills/connect/SKILL.md +156 -0
  624. package/.claude/skills/connect-apps/SKILL.md +80 -0
  625. package/.claude/skills/content-research-writer/SKILL.md +538 -0
  626. package/.claude/skills/continuous-agent-loop/SKILL.md +187 -0
  627. package/.claude/skills/continuous-learning-v2/SKILL.md +238 -0
  628. package/.claude/skills/continuous-learning-v2/config.json +35 -0
  629. package/.claude/skills/continuous-learning-v2/hooks/observe.sh +333 -0
  630. package/.claude/skills/continuous-learning-v2/instinct-cli.py +406 -0
  631. package/.claude/skills/domain-name-brainstormer/SKILL.md +212 -0
  632. package/.claude/skills/eval-harness/SKILL.md +100 -0
  633. package/.claude/skills/eval-harness/swe-bench/README.md +80 -0
  634. package/.claude/skills/eval-harness/swe-bench/config.yml +29 -0
  635. package/.claude/skills/eval-harness/swe-bench/docker/Dockerfile +25 -0
  636. package/.claude/skills/eval-harness/swe-bench/docker/docker-compose.yml +18 -0
  637. package/.claude/skills/eval-harness/swe-bench/results/dry-run-2026-05-04.json +137 -0
  638. package/.claude/skills/eval-harness/swe-bench/results/dry-run-comparison-2026-05-04.md +112 -0
  639. package/.claude/skills/eval-harness/swe-bench/results/dry-run-improved-2026-05-04.json +165 -0
  640. package/.claude/skills/eval-harness/swe-bench/results/raw/astropy__astropy-12907.patch +12 -0
  641. package/.claude/skills/eval-harness/swe-bench/results/raw/astropy__astropy-12907.txt +322 -0
  642. package/.claude/skills/eval-harness/swe-bench/results/raw/astropy__astropy-12907.whole-file.txt +322 -0
  643. package/.claude/skills/eval-harness/swe-bench/runner.py +845 -0
  644. package/.claude/skills/eval-harness/swe-bench/scoring.py +298 -0
  645. package/.claude/skills/eval-harness/swe-bench/tasks/fetch_tasks.py +81 -0
  646. package/.claude/skills/eval-harness/swe-bench/tasks/lite-50.json +702 -0
  647. package/.claude/skills/file-organizer/SKILL.md +433 -0
  648. package/.claude/skills/gan-style-harness/SKILL.md +111 -0
  649. package/.claude/skills/gateguard/.gateguard.yml +47 -0
  650. package/.claude/skills/gateguard/SKILL.md +99 -0
  651. package/.claude/skills/internal-comms/LICENSE.txt +202 -0
  652. package/.claude/skills/internal-comms/SKILL.md +32 -0
  653. package/.claude/skills/internal-comms/examples/3p-updates.md +47 -0
  654. package/.claude/skills/internal-comms/examples/company-newsletter.md +65 -0
  655. package/.claude/skills/internal-comms/examples/faq-answers.md +30 -0
  656. package/.claude/skills/internal-comms/examples/general-comms.md +16 -0
  657. package/.claude/skills/invoice-organizer/SKILL.md +446 -0
  658. package/.claude/skills/karpathy-guidelines/SKILL.md +67 -0
  659. package/.claude/skills/langsmith-fetch/SKILL.md +485 -0
  660. package/.claude/skills/lead-research-assistant/SKILL.md +199 -0
  661. package/.claude/skills/mcp-builder/LICENSE.txt +202 -0
  662. package/.claude/skills/mcp-builder/SKILL.md +328 -0
  663. package/.claude/skills/mcp-builder/reference/evaluation.md +602 -0
  664. package/.claude/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
  665. package/.claude/skills/mcp-builder/reference/node_mcp_server.md +916 -0
  666. package/.claude/skills/mcp-builder/reference/python_mcp_server.md +752 -0
  667. package/.claude/skills/mcp-builder/scripts/connections.py +151 -0
  668. package/.claude/skills/mcp-builder/scripts/evaluation.py +373 -0
  669. package/.claude/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  670. package/.claude/skills/mcp-builder/scripts/requirements.txt +2 -0
  671. package/.claude/skills/raffle-winner-picker/SKILL.md +159 -0
  672. package/.claude/skills/repo-map/README.md +125 -0
  673. package/.claude/skills/repo-map/SKILL.md +128 -0
  674. package/.claude/skills/repo-map/examples/sample-output.md +1194 -0
  675. package/.claude/skills/repo-map/repo-map.py +715 -0
  676. package/.claude/skills/salesforce-e2e-testing/SKILL.md +116 -0
  677. package/.claude/skills/salesforce-e2e-testing/catalog-template.md +161 -0
  678. package/.claude/skills/salesforce-e2e-testing/methodology.md +179 -0
  679. package/.claude/skills/salesforce-e2e-testing/observation-rules.md +280 -0
  680. package/.claude/skills/salesforce-e2e-testing/pattern-taxonomy.md +392 -0
  681. package/.claude/skills/salesforce-e2e-testing/procedure-template.md +376 -0
  682. package/.claude/skills/skill-creator/LICENSE.txt +202 -0
  683. package/.claude/skills/skill-creator/SKILL.md +209 -0
  684. package/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
  685. package/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
  686. package/.claude/skills/skill-creator/scripts/quick_validate.py +65 -0
  687. package/.claude/skills/skill-share/SKILL.md +80 -0
  688. package/.claude/skills/tailored-resume-generator/SKILL.md +345 -0
  689. package/.claude/skills/template-skill/SKILL.md +6 -0
  690. package/.claude/skills/theme-factory/LICENSE.txt +202 -0
  691. package/.claude/skills/theme-factory/SKILL.md +59 -0
  692. package/.claude/skills/theme-factory/theme-showcase.pdf +0 -0
  693. package/.claude/skills/theme-factory/themes/arctic-frost.md +19 -0
  694. package/.claude/skills/theme-factory/themes/botanical-garden.md +19 -0
  695. package/.claude/skills/theme-factory/themes/desert-rose.md +19 -0
  696. package/.claude/skills/theme-factory/themes/forest-canopy.md +19 -0
  697. package/.claude/skills/theme-factory/themes/golden-hour.md +19 -0
  698. package/.claude/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  699. package/.claude/skills/theme-factory/themes/modern-minimalist.md +19 -0
  700. package/.claude/skills/theme-factory/themes/ocean-depths.md +19 -0
  701. package/.claude/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  702. package/.claude/skills/theme-factory/themes/tech-innovation.md +19 -0
  703. package/.claude/skills/verification-loop/SKILL.md +129 -0
  704. package/.claude/skills/webapp-testing/LICENSE.txt +202 -0
  705. package/.claude/skills/webapp-testing/SKILL.md +96 -0
  706. package/.claude/skills/webapp-testing/examples/console_logging.py +35 -0
  707. package/.claude/skills/webapp-testing/examples/element_discovery.py +40 -0
  708. package/.claude/skills/webapp-testing/examples/static_html_automation.py +33 -0
  709. package/.claude/skills/webapp-testing/scripts/with_server.py +106 -0
  710. package/.claude/templates/docs/draft/_DRAFT_TEMPLATE.md +162 -0
  711. package/.claude/templates/docs/draft/_TEST_DESIGN_TEMPLATE.md +76 -0
  712. package/.claude/templates/docs/tasks/_TASK_TEMPLATE.md +276 -0
  713. package/.claude/templates/docs/tasks/list.md +80 -0
  714. package/.claude/templates/docs/tasks/parking-lot.md +82 -0
  715. package/.claude/templates/settings.user-level.json.template +306 -0
  716. package/.claude/tests/SMOKE-CLASSIFICATION.md +199 -0
  717. package/.claude/tests/action-space-count-smoke.sh +130 -0
  718. package/.claude/tests/agent-router-suggest-wiring-smoke.sh +188 -0
  719. package/.claude/tests/audit-followups-smoke.sh +158 -0
  720. package/.claude/tests/autonomous-action-guard-relaxation-smoke.sh +479 -0
  721. package/.claude/tests/autonomous-action-guard-smoke.sh +187 -0
  722. package/.claude/tests/check-serena-mcp-smoke.sh +156 -0
  723. package/.claude/tests/common-rules-import-smoke.sh +209 -0
  724. package/.claude/tests/confidence-gate-smoke.sh +220 -0
  725. package/.claude/tests/config-feature-toggles-smoke.sh +389 -0
  726. package/.claude/tests/context-budget-smoke.sh +222 -0
  727. package/.claude/tests/custom-pm-commands-smoke.sh +93 -0
  728. package/.claude/tests/delegation-guard-code-smoke.sh +244 -0
  729. package/.claude/tests/delegation-guard-deny-layers-smoke.sh +356 -0
  730. package/.claude/tests/delegation-guard-readonly-filter-smoke.sh +205 -0
  731. package/.claude/tests/delegation-guard-search-whitelist-smoke.sh +152 -0
  732. package/.claude/tests/delegation-guard-segment-smoke.sh +109 -0
  733. package/.claude/tests/dispatcher-blocker-invariance-smoke.sh +700 -0
  734. package/.claude/tests/dispatcher-core-smoke.sh +452 -0
  735. package/.claude/tests/dispatcher-merge-matrix-smoke.sh +825 -0
  736. package/.claude/tests/dispatcher-success-stdout-smoke.sh +290 -0
  737. package/.claude/tests/draft-flow-guard-approved-dir-smoke.sh +234 -0
  738. package/.claude/tests/draft-flow-guard-smoke.sh +194 -0
  739. package/.claude/tests/dual-mode-portability-smoke.sh +131 -0
  740. package/.claude/tests/effective-hook-matrix-smoke.sh +261 -0
  741. package/.claude/tests/enforcement-mismatch-smoke.sh +263 -0
  742. package/.claude/tests/fixtures/cascade-sample.jsonl +9 -0
  743. package/.claude/tests/fixtures/next-actions/case-clean.md +14 -0
  744. package/.claude/tests/fixtures/next-actions/case-with-red.md +16 -0
  745. package/.claude/tests/fixtures/next-actions/case-with-yellow-only.md +14 -0
  746. package/.claude/tests/fixtures/normal-broken-scatter.jsonl +5 -0
  747. package/.claude/tests/fixtures/task-71/blocker-baseline.tsv +24 -0
  748. package/.claude/tests/fixtures/task-71/settings-inventory.tsv +37 -0
  749. package/.claude/tests/fixtures/transcript-50pct.jsonl +2 -0
  750. package/.claude/tests/fixtures/transcript-60pct.jsonl +2 -0
  751. package/.claude/tests/fixtures/transcript-80pct.jsonl +2 -0
  752. package/.claude/tests/fixtures/transcript-95pct.jsonl +2 -0
  753. package/.claude/tests/fixtures/workflow-guard/case-2-mid.json +21 -0
  754. package/.claude/tests/fixtures/workflow-guard/case-3-blocked.json +33 -0
  755. package/.claude/tests/fixtures/workflow-guard/case-4-clean.json +27 -0
  756. package/.claude/tests/fixtures/workflow-guard/case-8-modify.json +23 -0
  757. package/.claude/tests/fixtures/workflow-guard/inputs/case-1.json +1 -0
  758. package/.claude/tests/fixtures/workflow-guard/inputs/case-2.json +1 -0
  759. package/.claude/tests/fixtures/workflow-guard/inputs/case-3.json +1 -0
  760. package/.claude/tests/fixtures/workflow-guard/inputs/case-4.json +1 -0
  761. package/.claude/tests/fixtures/workflow-guard/inputs/case-5.json +1 -0
  762. package/.claude/tests/fixtures/workflow-guard/inputs/case-6.json +1 -0
  763. package/.claude/tests/fixtures/workflow-guard/inputs/case-7.json +1 -0
  764. package/.claude/tests/fixtures/workflow-guard/inputs/case-8.json +1 -0
  765. package/.claude/tests/gateguard-smoke.sh +213 -0
  766. package/.claude/tests/git-deny-mainline-policy-smoke.sh +222 -0
  767. package/.claude/tests/harness-audit-c-batch-smoke.sh +270 -0
  768. package/.claude/tests/harness-audit-compare-smoke.sh +186 -0
  769. package/.claude/tests/harness-audit-pipeline-health-smoke.sh +326 -0
  770. package/.claude/tests/harness-config-local-smoke.sh +232 -0
  771. package/.claude/tests/hc-config-git-policy-smoke.sh +241 -0
  772. package/.claude/tests/hc-config-key-parity-smoke.sh +149 -0
  773. package/.claude/tests/hc-config-migration-smoke.sh +251 -0
  774. package/.claude/tests/hc-config-script-smoke.sh +1106 -0
  775. package/.claude/tests/hc-config-tui-smoke.sh +801 -0
  776. package/.claude/tests/hc-config-web-ui-smoke.sh +3224 -0
  777. package/.claude/tests/hook-cwd-robustness-smoke.sh +206 -0
  778. package/.claude/tests/hook-frequency-tweaks-smoke.sh +312 -0
  779. package/.claude/tests/improvement-proposal-cache-smoke.sh +238 -0
  780. package/.claude/tests/install-sh-overwrite-all-smoke.sh +274 -0
  781. package/.claude/tests/install-sh-regen-settings-smoke.sh +301 -0
  782. package/.claude/tests/install-sh-sync-drift-smoke.sh +285 -0
  783. package/.claude/tests/layer-b-context-isolation-smoke.sh +392 -0
  784. package/.claude/tests/list-md-plan-first-reminder-smoke.sh +313 -0
  785. package/.claude/tests/loop-auto-progress-smoke.sh +372 -0
  786. package/.claude/tests/loop-confirmation-detector-smoke.sh +674 -0
  787. package/.claude/tests/new-task-batch-update-smoke.sh +664 -0
  788. package/.claude/tests/next-actions-hooks-smoke.sh +283 -0
  789. package/.claude/tests/npx-cli-smoke.sh +696 -0
  790. package/.claude/tests/observe-flock-smoke.sh +223 -0
  791. package/.claude/tests/observe-jq-parse-smoke.sh +250 -0
  792. package/.claude/tests/observe-repair-smoke.sh +475 -0
  793. package/.claude/tests/observe-rotate-smoke.sh +428 -0
  794. package/.claude/tests/observe-subagent-stop-smoke.sh +476 -0
  795. package/.claude/tests/parallel-subagent-reminder-smoke.sh +918 -0
  796. package/.claude/tests/project-root-smoke.sh +140 -0
  797. package/.claude/tests/project-rules-protection-smoke.sh +199 -0
  798. package/.claude/tests/review-required-min-count-smoke.sh +286 -0
  799. package/.claude/tests/reviewer-count-guard-smoke.sh +490 -0
  800. package/.claude/tests/rule-architecture-smoke.sh +418 -0
  801. package/.claude/tests/rule-change-draft-flow-guard-smoke.sh +343 -0
  802. package/.claude/tests/run-all-smokes.sh +340 -0
  803. package/.claude/tests/session-help-surface-smoke.sh +224 -0
  804. package/.claude/tests/session-start-parallel-smoke.sh +165 -0
  805. package/.claude/tests/sessionstart-budget-smoke.sh +185 -0
  806. package/.claude/tests/sessionstart-footprint-smoke.sh +258 -0
  807. package/.claude/tests/settings-dispatcher-baseline-smoke.sh +709 -0
  808. package/.claude/tests/settings-generation-feature-pruning-smoke.sh +196 -0
  809. package/.claude/tests/stale-harness-detect-smoke.sh +974 -0
  810. package/.claude/tests/statusline-smoke.sh +180 -0
  811. package/.claude/tests/task-rule-guard-smoke.sh +656 -0
  812. package/.claude/tests/tool-call-slip-detector-smoke.sh +101 -0
  813. package/.claude/tests/wave-precheck-template-smoke.sh +159 -0
  814. package/.claude/tests/why-x5-violation-detect-smoke.sh +157 -0
  815. package/.claude/tests/workflow-guard-smoke.sh +266 -0
  816. package/CLAUDE.md +75 -0
  817. package/LICENSE +21 -0
  818. package/README.md +790 -0
  819. package/bin/cli.js +395 -0
  820. package/docs/INVENTORY.md +163 -0
  821. package/install.sh +769 -0
  822. package/package.json +25 -0
@@ -0,0 +1,3224 @@
1
+ #!/usr/bin/env bash
2
+ # .claude/tests/hc-config-web-ui-smoke.sh — task-63 Step 5 (設計簡素化 案 C: /api/preset/save 撤去 + /api/current-preset 追加)
3
+ #
4
+ # 目的:
5
+ # hc-config Web UI (hc-config-web-server.js) の動作を 34 case + 手動 4 case コメント で検証。
6
+ # task-63: /api/preset/save 関連 case (S-22/S-25b/S-26/S-31/S-33) 撤去、
7
+ # /api/current-preset / top view / edit 遷移 / unsaved banner の 5 case (S-35〜S-39) 追加。
8
+ #
9
+ # Case 一覧 (自動 30 + legacy fallback 2 + manual SKIP 4):
10
+ # server lifecycle (4):
11
+ # S-01: server 起動 (HC_WEB_NO_OPEN=1) → port 3060 LISTEN → GET / 200 or 302 → kill
12
+ # S-02: port 3060 先 occupy → server → 3061 以降で listen → cleanup
13
+ # S-03: port 3060-3070 全 occupied → server process.exit(1)
14
+ # S-04: server 起動 → SIGINT → graceful shutdown → port release
15
+ # static 配信 (4):
16
+ # S-05: GET / → 302 or 200
17
+ # S-06: GET /static/index.html → 200 + text/html
18
+ # S-07: GET /static/app.js → 200 + application/javascript
19
+ # S-08: path traversal GET /static/../../.claude/harness-config.yml → 403/404
20
+ # API endpoint (5):
21
+ # S-09: GET /api/categories → 200 + .categories length >= 1
22
+ # S-10: GET /api/keys → 200 + .keys length >= 1
23
+ # S-11: GET /api/presets → 200 + .presets length == 10
24
+ # S-12: GET /api/preset/poc-no-git/diff → 200 + .changes array + key/current/new/effect fields
25
+ # S-13: POST /api/set 不正 key (空/欠落) → 400
26
+ # preset apply / rollback (3):
27
+ # S-14: POST /api/preset/poc-no-git/apply → 200/207 + history file 生成 (HISTORY_DIR isolated)
28
+ # S-15: apply → POST /api/preset/rollback/<ts> → 200 + ok:true + restored==applied_count
29
+ # S-16: rollback timestamp traversal → 400
30
+ # legacy fallback + edge (2):
31
+ # S-17: HC_HC_CONFIG_TUI_LEGACY=true → TUI 経路 dispatcher stderr ログ確認 (厳密化)
32
+ # S-18: node 不在 → WARN stderr + TUI fallback
33
+ # body size (1):
34
+ # S-19: 1MB+1byte body POST /api/set → 400/413
35
+ #
36
+ # iter 4 C 新規 case (task-61 由来):
37
+ # S-20: abort rollback silent no-op verify
38
+ # S-21: unknown preset 404 path
39
+ # S-22: 撤去 (/api/preset/save 撤去 — task-63 設計簡素化)
40
+ # S-23: partial failure ok:false + rollback 件数 verify
41
+ # S-24: HISTORY_DIR 不在 → 自動作成 verify
42
+ # S-25: invalid JSON body → 400 (/api/set のみ、/api/preset/save sub-case は撤去)
43
+ # S-26: 撤去 (/api/preset/save 6 軸欠落 — task-63 設計簡素化)
44
+ # S-27: /api/set 空文字列 value (empty string) → 仕様確認
45
+ # S-28: URL encoded traversal /api/preset/rollback/..%2F..%2F → 400
46
+ # S-29: GET /api/preset/history → .history array
47
+ # S-30: 0 件 rollback (targets=[]) → ok:true + restored:0
48
+ # S-31: 撤去 (/api/preset/save path traversal — task-63 設計簡素化)
49
+ # S-32: category filter GET /api/keys?category=<name> → filtered list
50
+ # S-33: 撤去 (XSS injection save name — task-63 設計簡素化)
51
+ # S-34: SIGTERM graceful shutdown → port release
52
+ #
53
+ # task-63 Step 5 新規 case (/api/current-preset + top/edit view + unsaved banner):
54
+ # S-35: GET /api/current-preset → 200 + match_type + display_name_ja field 含む
55
+ # S-36: preset apply 後 GET /api/current-preset → match_type=preset + 正しい name/display_name_ja
56
+ # S-37: app.js に renderTop 関数 + bannerLabel/bannerValue が静的に存在 (top view banner 描画確認)
57
+ # S-38: app.js に state.view='edit' 遷移ロジック + renderEdit 関数が静的に存在 (edit view 遷移確認)
58
+ # S-39: /api/set で 1 key 変更 → GET /api/current-preset → match_type=unsaved (未保存変更あり)
59
+ #
60
+ # 手動 case (smoke 内にコメントとして記載、Step 6 実施):
61
+ # M-01: browser で preset 選択 → diff preview → checkbox toggle → Apply → history 追加
62
+ # M-02: category 選択 → key 一覧 → 編集 → Apply → 値反映確認
63
+ # M-03: Rollback ボタン → confirm dialog → 確認 → 元値復元
64
+ # M-04: Tailwind CDN offline で degradation 動作確認
65
+ #
66
+ # task-63 Step 6 iter-2 fix 新規 case:
67
+ # S-40: POST /api/preset/save → 404 (custom 保存撤去 regression guard、F4)
68
+ # S-41: UI 3 file (index.html/app.js/style.css) に絵文字 0 件 (絵文字不要 regression guard、F5)
69
+ #
70
+ # task-76 Step 6-fix (2 分割再設計に合わせた stale test 整理):
71
+ # RETIRED (常時 SKIP、return 2):
72
+ # S-37 (renderTop/banner) / S-38 (edit-view state machine) /
73
+ # S-45 (top-view 6 軸 axes table) / S-46 (edit view / applyPresetMode)。
74
+ # 理由: top/edit 2-view を廃止し 2 分割 1 画面 (左 preset / 右 category accordion) へ全面再設計。
75
+ # 旧 UI symbol (renderTop/renderEdit/view:'edit'/applyPresetMode/cp.axes/AXIS_LABELS_JA) は新設計に不在。
76
+ # RE-TARGET:
77
+ # S-42 (DOM id 契約 cross-check) — 新 app.js は getElementById を $(id) wrapper 経由で呼ぶため
78
+ # $('id') 抽出を追加、anchor を新 render target (preset-list/category-accordion/panel-config/
79
+ # panel-history/history-tbody/save-bar) に更新。cross-file 契約乖離の安全網を新構造で機能させる。
80
+ # 除外規則:
81
+ # S-41 (絵文字 0 件 guard) は ★ (U+2605 BLACK STAR) を検出範囲から除外
82
+ # (設計 §3 が「★ カスタム」を明示採用、BLACK STAR は emoji でなく typographic dingbat)。
83
+ # 新規維持: S-48〜S-51 (task-76 Step 1-2)。
84
+ #
85
+ # 設計:
86
+ # - subshell 関数 ( set -uo pipefail; ... ) で各 case を隔離
87
+ # - file-top に set -euo pipefail を書かない (feedback_set_e_in_sourced_libs 規範)
88
+ # - color output (TTY 検出で plain 切替)
89
+ # - trap で server cleanup 確実化
90
+ # - HC_WEB_NO_OPEN=1 で browser auto-open 抑止
91
+ # - HC_HISTORY_DIR_OVERRIDE で history dir を TMP_DIR 内に隔離 (test isolation)
92
+ #
93
+ # 重要制約:
94
+ # - bash 3.2 互換 (associative array 禁止、[ ] のみ)
95
+ # - BSD/GNU bash 両対応 (macOS bash 3.2 + Linux bash 5.x)
96
+
97
+ set -uo pipefail
98
+
99
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
100
+ REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
101
+
102
+ # ---- top-level network-unavailable skip guard (task-74 Step 2) ----
103
+ # sandbox/CI 環境で localhost port bind が不可、または node が不在の場合、
104
+ # 全 case を SKIP (exit 0) して FAIL と混同しないようにする。
105
+ # per-case `_has_node` skip は残存 (両立設計)。
106
+ #
107
+ # skip 条件 (OR):
108
+ # a) HC_SMOKE_SKIP_NETWORK=1 が設定されている (明示 opt-out)
109
+ # b) node コマンドが存在しない (ほぼ全 case が node 依存)
110
+ # c) localhost port bind probe が失敗する (sandbox/CI で port bind 不可)
111
+ #
112
+ # 自動 probe (c): python3 or nc で 127.0.0.1:0 への一時 bind を試み、
113
+ # 失敗時に sandbox/CI と判定して SKIP。
114
+ _network_available() {
115
+ # python3 で localhost bind probe
116
+ if command -v python3 >/dev/null 2>&1; then
117
+ python3 -c "
118
+ import socket, sys
119
+ s = socket.socket()
120
+ try:
121
+ s.bind(('127.0.0.1', 0))
122
+ s.close()
123
+ sys.exit(0)
124
+ except Exception:
125
+ sys.exit(1)
126
+ " >/dev/null 2>&1 && return 0
127
+ fi
128
+ # nc で簡易確認 (nc -l 0 は環境依存のため listen 試行)
129
+ # 代替: /dev/tcp が使えるなら connect で確認 (bind 確認は難しいため python3 優先)
130
+ # python3 不在 + nc 不在 = probe 不可 → SKIP safe-default
131
+ return 1
132
+ }
133
+
134
+ _top_level_network_skip() {
135
+ local reason=""
136
+ if [ "${HC_SMOKE_SKIP_NETWORK:-0}" = "1" ]; then
137
+ reason="HC_SMOKE_SKIP_NETWORK=1 (explicit opt-out)"
138
+ elif ! command -v node >/dev/null 2>&1; then
139
+ reason="node not available (required for Web UI server)"
140
+ elif ! _network_available; then
141
+ reason="localhost port bind probe failed (sandbox/CI environment)"
142
+ fi
143
+ if [ -n "$reason" ]; then
144
+ printf '\n%s\n' "=== hc-config-web-ui-smoke ==="
145
+ printf 'SKIP: network-unavailable environment detected — %s\n' "$reason"
146
+ printf 'All cases skipped (exit 0). Use HC_SMOKE_SKIP_NETWORK=0 to force attempt.\n'
147
+ exit 0
148
+ fi
149
+ }
150
+
151
+ # top-level skip guard 実行 (TMP_DIR 作成前に評価)
152
+ _top_level_network_skip
153
+ HC_CONFIG_SCRIPT="${REPO_ROOT}/.claude/scripts/hc-config.sh"
154
+ WEB_SERVER="${REPO_ROOT}/.claude/scripts/lib/hc-config-web-server.js"
155
+ # iter 4 C: T-H2 — HISTORY_DIR は TMP_DIR 内に隔離 (test isolation)
156
+ # server.js が HC_HISTORY_DIR_OVERRIDE を読んで切替える (領域 A 実装前提)
157
+ # 実装前は fallback として元の HISTORY_DIR も保持
158
+ HISTORY_DIR="${REPO_ROOT}/.claude/.preset-history"
159
+
160
+ # tmp dir (cleanup on exit)
161
+ TMP_DIR="$(mktemp -d "/tmp/hc-config-web-ui-smoke.XXXXXX")"
162
+
163
+ # F2 (iter-2 fix): smoke は S-36/S-39 で poc-no-git apply + confidence_threshold set により
164
+ # 実 .claude/harness-config.yml を永続変更する。teardown で原状復帰させるため起動時に snapshot を取り、
165
+ # _cleanup (EXIT trap) で restore する。これで smoke 実行後も yml に差分が残らない。
166
+ HARNESS_CONFIG="${REPO_ROOT}/.claude/harness-config.yml"
167
+ HARNESS_CONFIG_SNAPSHOT="${TMP_DIR}/harness-config.yml.snapshot"
168
+ if [ -f "${HARNESS_CONFIG}" ]; then
169
+ cp "${HARNESS_CONFIG}" "${HARNESS_CONFIG_SNAPSHOT}" 2>/dev/null || true
170
+ fi
171
+
172
+ # isolated history dir for test
173
+ ISOLATED_HISTORY_DIR="${TMP_DIR}/.preset-history"
174
+
175
+ # iter 6 B: isolated presets dir (S-22/S-31/S-33 teardown — custom-test-*.yml pollution 解消)
176
+ ISOLATED_PRESETS_DIR="${TMP_DIR}/presets"
177
+
178
+ SERVER_PID=""
179
+
180
+ # ---- port cleanup helpers ----
181
+
182
+ _get_server_port() {
183
+ # LOG_FILE から "server started on http://127.0.0.1:<port>/" を抽出
184
+ local log_file="$1"
185
+ grep -oE 'http://127\.0\.0\.1:[0-9]+/' "$log_file" 2>/dev/null | head -1 | grep -oE '[0-9]+' | tail -1 || true
186
+ }
187
+
188
+ _cleanup() {
189
+ if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
190
+ kill "$SERVER_PID" 2>/dev/null || true
191
+ sleep 0.5
192
+ kill -9 "$SERVER_PID" 2>/dev/null || true
193
+ fi
194
+ SERVER_PID=""
195
+ # F2 (iter-2 fix): TMP_DIR 削除前に harness-config.yml を snapshot から restore
196
+ # (S-36/S-39 が apply/set で実 yml を変更するため、原状復帰させる)
197
+ if [ -f "${HARNESS_CONFIG_SNAPSHOT}" ]; then
198
+ cp "${HARNESS_CONFIG_SNAPSHOT}" "${HARNESS_CONFIG}" 2>/dev/null || true
199
+ # apply/set 経由で hc-config.sh が生成した atomic backup (harness-config.yml.bak.*) を掃除
200
+ # (snapshot restore は yml 本体のみ。.bak.* 兄弟 file は別 side-effect なので明示削除)
201
+ rm -f "${HARNESS_CONFIG}".bak.* 2>/dev/null || true
202
+ fi
203
+ # iter 4 C: ISOLATED_HISTORY_DIR も含めて TMP_DIR 全体を削除
204
+ rm -rf "$TMP_DIR"
205
+ }
206
+
207
+ trap '_cleanup' EXIT INT TERM
208
+
209
+ # ---- color output ----
210
+ if [ -t 1 ]; then
211
+ _GREEN='\033[0;32m'
212
+ _RED='\033[0;31m'
213
+ _YELLOW='\033[0;33m'
214
+ _NC='\033[0m'
215
+ else
216
+ _GREEN=''
217
+ _RED=''
218
+ _YELLOW=''
219
+ _NC=''
220
+ fi
221
+
222
+ PASS=0
223
+ FAIL=0
224
+ SKIP=0
225
+ FAILED_CASES=""
226
+
227
+ _record() {
228
+ local result="$1"
229
+ local case_id="$2"
230
+ local desc="$3"
231
+ if [ "$result" = "PASS" ]; then
232
+ printf " ${_GREEN}PASS${_NC} Case %s: %s\n" "$case_id" "$desc"
233
+ PASS=$((PASS + 1))
234
+ elif [ "$result" = "SKIP" ]; then
235
+ printf " ${_YELLOW}SKIP${_NC} Case %s: %s\n" "$case_id" "$desc"
236
+ SKIP=$((SKIP + 1))
237
+ else
238
+ printf " ${_RED}FAIL${_NC} Case %s: %s\n" "$case_id" "$desc"
239
+ FAIL=$((FAIL + 1))
240
+ FAILED_CASES="${FAILED_CASES} ${case_id}"
241
+ fi
242
+ }
243
+
244
+ # node コマンドが使えるか確認
245
+ _has_node() {
246
+ command -v node >/dev/null 2>&1
247
+ }
248
+
249
+ # timeout fallback (macOS 等で `timeout` 不在の環境向け)
250
+ if ! command -v timeout >/dev/null 2>&1; then
251
+ if command -v perl >/dev/null 2>&1; then
252
+ timeout() {
253
+ local sec="$1"; shift
254
+ perl -e 'alarm shift; exec @ARGV or exit 127' "$sec" "$@"
255
+ }
256
+ else
257
+ timeout() {
258
+ shift
259
+ "$@"
260
+ }
261
+ fi
262
+ fi
263
+
264
+ # サーバー起動ヘルパー: LOG_FILE にポートを書く、SERVER_PID をセット
265
+ # $1: log_file
266
+ # $2: (optional) HC_HISTORY_DIR_OVERRIDE path
267
+ _start_server() {
268
+ local log_file="$1"
269
+ local hist_dir="${2:-}"
270
+ if [ -n "$hist_dir" ]; then
271
+ HC_WEB_NO_OPEN=1 HC_HISTORY_DIR_OVERRIDE="$hist_dir" node "${WEB_SERVER}" >"$log_file" 2>&1 &
272
+ else
273
+ HC_WEB_NO_OPEN=1 node "${WEB_SERVER}" >"$log_file" 2>&1 &
274
+ fi
275
+ SERVER_PID=$!
276
+ # 最大 8 秒待機 (port bind まで)
277
+ local waited=0
278
+ while [ $waited -lt 8 ]; do
279
+ sleep 0.5
280
+ waited=$((waited + 1))
281
+ if grep -q 'server started' "$log_file" 2>/dev/null; then
282
+ break
283
+ fi
284
+ if ! kill -0 "$SERVER_PID" 2>/dev/null; then
285
+ break
286
+ fi
287
+ done
288
+ }
289
+
290
+ # ポートを占有するためのリスナーをバックグラウンドで起動
291
+ # python3 優先 (SO_REUSEADDR なし = Node.js が奪取できない確実な占有)
292
+ # macOS の nc -l は Node.js SO_REUSEADDR で奪われるため使用不可
293
+ _occupy_port() {
294
+ local port="$1"
295
+ if command -v python3 >/dev/null 2>&1; then
296
+ python3 -c "
297
+ import socket, time
298
+ s = socket.socket()
299
+ # SO_REUSEADDR を設定しない → Node.js が EADDRINUSE を返せる
300
+ s.bind(('127.0.0.1', $port))
301
+ s.listen(1)
302
+ time.sleep(30)
303
+ s.close()
304
+ " >/dev/null 2>&1 &
305
+ local pid=$!
306
+ # iter 4 C: T-H3 — bind 確認ループ (最大 5 回 retry)
307
+ local retry=0
308
+ while [ $retry -lt 5 ]; do
309
+ sleep 0.2
310
+ # nc -z が存在すればポート確認、なければ python3 で確認
311
+ if command -v nc >/dev/null 2>&1; then
312
+ if nc -z 127.0.0.1 "$port" >/dev/null 2>&1; then
313
+ printf '%s' "$pid"
314
+ return 0
315
+ fi
316
+ else
317
+ if python3 -c "
318
+ import socket
319
+ s = socket.socket()
320
+ try:
321
+ s.connect(('127.0.0.1', $port))
322
+ s.close()
323
+ exit(0)
324
+ except:
325
+ exit(1)
326
+ " >/dev/null 2>&1; then
327
+ printf '%s' "$pid"
328
+ return 0
329
+ fi
330
+ fi
331
+ retry=$((retry + 1))
332
+ done
333
+ # bind 確認できなかったが pid は返す (best-effort)
334
+ printf '%s' "$pid"
335
+ return 0
336
+ fi
337
+ if command -v socat >/dev/null 2>&1; then
338
+ socat -u TCP-LISTEN:"$port" STDOUT >/dev/null 2>&1 &
339
+ printf '%s' $!
340
+ return 0
341
+ fi
342
+ printf ''
343
+ }
344
+
345
+ # curl が JSON を返すか確認 (jq 不要、grep で key 確認)
346
+ _curl_json() {
347
+ local url="$1"
348
+ curl -s --connect-timeout 3 --max-time 5 "$url" 2>/dev/null || true
349
+ }
350
+
351
+ _curl_post_json() {
352
+ local url="$1"
353
+ local body="$2"
354
+ curl -s --connect-timeout 3 --max-time 5 \
355
+ -X POST -H 'Content-Type: application/json' \
356
+ -d "$body" "$url" 2>/dev/null || true
357
+ }
358
+
359
+ _curl_post_json_code() {
360
+ local url="$1"
361
+ local body="$2"
362
+ curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 10 \
363
+ -X POST -H 'Content-Type: application/json' \
364
+ -d "$body" "$url" 2>/dev/null || true
365
+ }
366
+
367
+ # ============================================================
368
+ # Case S-01: server 起動 (HC_WEB_NO_OPEN=1) → port LISTEN → GET / 200/302 → kill
369
+ # ============================================================
370
+ _case_s01() (
371
+ set -uo pipefail
372
+
373
+ if ! _has_node; then
374
+ printf 'S-01: node not found, skip\n' >&2
375
+ return 2
376
+ fi
377
+ if [ ! -f "${WEB_SERVER}" ]; then
378
+ printf 'S-01: hc-config-web-server.js not found\n' >&2
379
+ return 1
380
+ fi
381
+
382
+ local log_file="${TMP_DIR}/s01-server.log"
383
+ _start_server "$log_file"
384
+ local pid=$SERVER_PID
385
+
386
+ if ! kill -0 "$pid" 2>/dev/null; then
387
+ printf 'S-01: server process not running\n' >&2
388
+ cat "$log_file" >&2
389
+ return 1
390
+ fi
391
+
392
+ local port
393
+ port=$(_get_server_port "$log_file")
394
+ if [ -z "$port" ]; then
395
+ printf 'S-01: could not detect server port from log\n' >&2
396
+ cat "$log_file" >&2
397
+ kill "$pid" 2>/dev/null || true
398
+ return 1
399
+ fi
400
+
401
+ # GET / → 200 or 302
402
+ local http_code
403
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
404
+ "http://127.0.0.1:${port}/" 2>/dev/null || true)
405
+
406
+ kill "$pid" 2>/dev/null || true
407
+ SERVER_PID=""
408
+
409
+ case "$http_code" in
410
+ 200|302) return 0 ;;
411
+ *)
412
+ printf 'S-01: GET / returned HTTP %s (expected 200 or 302)\n' "$http_code" >&2
413
+ return 1
414
+ ;;
415
+ esac
416
+ )
417
+
418
+ # ============================================================
419
+ # Case S-02: port 3060 先 occupy → server → 3061+ で listen
420
+ # ============================================================
421
+ _case_s02() (
422
+ set -uo pipefail
423
+
424
+ if ! _has_node; then
425
+ printf 'S-02: node not found, skip\n' >&2
426
+ return 2
427
+ fi
428
+
429
+ # port 3060 を占有
430
+ local occupy_pid
431
+ occupy_pid=$(_occupy_port 3060)
432
+ if [ -z "$occupy_pid" ]; then
433
+ printf 'S-02: no nc/socat/python3 to occupy port 3060, skip\n' >&2
434
+ return 2
435
+ fi
436
+ sleep 0.3
437
+
438
+ local log_file="${TMP_DIR}/s02-server.log"
439
+ _start_server "$log_file"
440
+ local srv_pid=$SERVER_PID
441
+
442
+ local port
443
+ port=$(_get_server_port "$log_file")
444
+
445
+ kill "$srv_pid" 2>/dev/null || true
446
+ kill "$occupy_pid" 2>/dev/null || true
447
+ SERVER_PID=""
448
+
449
+ if [ -z "$port" ]; then
450
+ printf 'S-02: server did not start on any port (log: %s)\n' "$log_file" >&2
451
+ cat "$log_file" >&2
452
+ return 1
453
+ fi
454
+
455
+ if [ "$port" = "3060" ]; then
456
+ printf 'S-02: server bound to 3060 despite it being occupied\n' >&2
457
+ return 1
458
+ fi
459
+
460
+ return 0
461
+ )
462
+
463
+ # ============================================================
464
+ # Case S-03: port 3060-3070 全 occupied → server process.exit(1)
465
+ # ============================================================
466
+ _case_s03() (
467
+ set -uo pipefail
468
+
469
+ if ! _has_node; then
470
+ printf 'S-03: node not found, skip\n' >&2
471
+ return 2
472
+ fi
473
+
474
+ local pids=""
475
+ local p
476
+ for p in 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070; do
477
+ local pid
478
+ pid=$(_occupy_port "$p")
479
+ if [ -n "$pid" ]; then
480
+ pids="${pids} ${pid}"
481
+ fi
482
+ done
483
+
484
+ if [ -z "$pids" ]; then
485
+ printf 'S-03: no port occupy tool available, skip\n' >&2
486
+ return 2
487
+ fi
488
+ sleep 0.5
489
+
490
+ local log_file="${TMP_DIR}/s03-server.log"
491
+ HC_WEB_NO_OPEN=1 timeout 10 node "${WEB_SERVER}" >"$log_file" 2>&1
492
+ local ec=$?
493
+
494
+ # cleanup occupied ports
495
+ for pid in $pids; do
496
+ kill "$pid" 2>/dev/null || true
497
+ done
498
+
499
+ if [ $ec -eq 0 ]; then
500
+ printf 'S-03: server exited 0 when all ports occupied (expected non-zero)\n' >&2
501
+ return 1
502
+ fi
503
+
504
+ return 0
505
+ )
506
+
507
+ # ============================================================
508
+ # Case S-04: server 起動 → SIGINT → graceful shutdown → port release
509
+ # ============================================================
510
+ _case_s04() (
511
+ set -uo pipefail
512
+
513
+ if ! _has_node; then
514
+ printf 'S-04: node not found, skip\n' >&2
515
+ return 2
516
+ fi
517
+
518
+ local log_file="${TMP_DIR}/s04-server.log"
519
+ _start_server "$log_file"
520
+ local pid=$SERVER_PID
521
+
522
+ if ! kill -0 "$pid" 2>/dev/null; then
523
+ printf 'S-04: server did not start\n' >&2
524
+ return 1
525
+ fi
526
+
527
+ local port
528
+ port=$(_get_server_port "$log_file")
529
+ if [ -z "$port" ]; then
530
+ printf 'S-04: could not detect port\n' >&2
531
+ kill "$pid" 2>/dev/null || true
532
+ SERVER_PID=""
533
+ return 1
534
+ fi
535
+
536
+ # SIGINT 送信
537
+ kill -INT "$pid" 2>/dev/null || true
538
+ local waited=0
539
+ while [ $waited -lt 6 ]; do
540
+ sleep 0.5
541
+ waited=$((waited + 1))
542
+ if ! kill -0 "$pid" 2>/dev/null; then
543
+ break
544
+ fi
545
+ done
546
+ SERVER_PID=""
547
+
548
+ if kill -0 "$pid" 2>/dev/null; then
549
+ printf 'S-04: server still running after SIGINT\n' >&2
550
+ kill -9 "$pid" 2>/dev/null || true
551
+ return 1
552
+ fi
553
+
554
+ # port が解放されているか: 別 server が同 port で起動できるか確認
555
+ local log2="${TMP_DIR}/s04-check.log"
556
+ HC_WEB_NO_OPEN=1 node "${WEB_SERVER}" >"$log2" 2>&1 &
557
+ local pid2=$!
558
+ sleep 2
559
+ local port2
560
+ port2=$(_get_server_port "$log2")
561
+ kill "$pid2" 2>/dev/null || true
562
+ wait "$pid2" 2>/dev/null || true
563
+
564
+ if [ -z "$port2" ]; then
565
+ printf 'S-04: port not released after SIGINT (second server could not start)\n' >&2
566
+ return 1
567
+ fi
568
+
569
+ return 0
570
+ )
571
+
572
+ # ============================================================
573
+ # shared: 共有 server インスタンス (S-05〜S-23、S-25〜S-33 共用)
574
+ # ============================================================
575
+ SHARED_PORT=""
576
+ SHARED_SERVER_PID=""
577
+ SHARED_LOG="${TMP_DIR}/shared-server.log"
578
+
579
+ _start_shared_server() {
580
+ local hist_dir="${1:-}"
581
+ local presets_dir="${2:-}"
582
+ if [ -n "$hist_dir" ] && [ -n "$presets_dir" ]; then
583
+ HC_WEB_NO_OPEN=1 HC_HISTORY_DIR_OVERRIDE="$hist_dir" HC_PRESETS_DIR_OVERRIDE="$presets_dir" node "${WEB_SERVER}" >"${SHARED_LOG}" 2>&1 &
584
+ elif [ -n "$hist_dir" ]; then
585
+ HC_WEB_NO_OPEN=1 HC_HISTORY_DIR_OVERRIDE="$hist_dir" node "${WEB_SERVER}" >"${SHARED_LOG}" 2>&1 &
586
+ else
587
+ HC_WEB_NO_OPEN=1 node "${WEB_SERVER}" >"${SHARED_LOG}" 2>&1 &
588
+ fi
589
+ SHARED_SERVER_PID=$!
590
+ SERVER_PID=$SHARED_SERVER_PID
591
+ local waited=0
592
+ while [ $waited -lt 8 ]; do
593
+ sleep 0.5
594
+ waited=$((waited + 1))
595
+ if grep -q 'server started' "${SHARED_LOG}" 2>/dev/null; then
596
+ break
597
+ fi
598
+ if ! kill -0 "$SHARED_SERVER_PID" 2>/dev/null; then
599
+ break
600
+ fi
601
+ done
602
+ SHARED_PORT=$(_get_server_port "${SHARED_LOG}")
603
+ }
604
+
605
+ _stop_shared_server() {
606
+ if [ -n "$SHARED_SERVER_PID" ] && kill -0 "$SHARED_SERVER_PID" 2>/dev/null; then
607
+ kill "$SHARED_SERVER_PID" 2>/dev/null || true
608
+ sleep 0.3
609
+ kill -9 "$SHARED_SERVER_PID" 2>/dev/null || true
610
+ fi
611
+ SHARED_SERVER_PID=""
612
+ SHARED_PORT=""
613
+ SERVER_PID=""
614
+ }
615
+
616
+ # ============================================================
617
+ # Case S-05: GET / → 302 redirect or 200
618
+ # ============================================================
619
+ _case_s05() (
620
+ set -uo pipefail
621
+ local port="$1"
622
+
623
+ local http_code
624
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
625
+ --max-redirs 0 "http://127.0.0.1:${port}/" 2>/dev/null || true)
626
+
627
+ case "$http_code" in
628
+ 200|302) return 0 ;;
629
+ *)
630
+ printf 'S-05: GET / returned HTTP %s (expected 200 or 302)\n' "$http_code" >&2
631
+ return 1
632
+ ;;
633
+ esac
634
+ )
635
+
636
+ # ============================================================
637
+ # Case S-06: GET /static/index.html → 200 + Content-Type text/html
638
+ # server.js は GET のみ /static/* を処理 (HEAD → 404 fallback) のため
639
+ # curl -sI (HEAD) ではなく GET + -D - でヘッダーを取得する
640
+ # ============================================================
641
+ _case_s06() (
642
+ set -uo pipefail
643
+ local port="$1"
644
+
645
+ # -D - で response headers を stdout に dump、-o /dev/null で body を捨てる
646
+ local headers
647
+ headers=$(curl -s -D - -o /dev/null --connect-timeout 3 --max-time 5 \
648
+ "http://127.0.0.1:${port}/static/index.html" 2>/dev/null || true)
649
+ local http_code
650
+ http_code=$(printf '%s' "$headers" | head -1 | grep -oE '[0-9]{3}' | head -1 || true)
651
+
652
+ if [ "$http_code" != "200" ]; then
653
+ printf 'S-06: GET /static/index.html returned HTTP %s (expected 200)\n' "$http_code" >&2
654
+ return 1
655
+ fi
656
+
657
+ if ! printf '%s' "$headers" | grep -qi 'content-type.*text/html'; then
658
+ printf 'S-06: Content-Type is not text/html\n' >&2
659
+ printf '%s\n' "$headers" >&2
660
+ return 1
661
+ fi
662
+
663
+ return 0
664
+ )
665
+
666
+ # ============================================================
667
+ # Case S-07: GET /static/app.js → 200 + Content-Type application/javascript
668
+ # ============================================================
669
+ _case_s07() (
670
+ set -uo pipefail
671
+ local port="$1"
672
+
673
+ local headers
674
+ headers=$(curl -s -D - -o /dev/null --connect-timeout 3 --max-time 5 \
675
+ "http://127.0.0.1:${port}/static/app.js" 2>/dev/null || true)
676
+ local http_code
677
+ http_code=$(printf '%s' "$headers" | head -1 | grep -oE '[0-9]{3}' | head -1 || true)
678
+
679
+ if [ "$http_code" != "200" ]; then
680
+ printf 'S-07: GET /static/app.js returned HTTP %s (expected 200)\n' "$http_code" >&2
681
+ return 1
682
+ fi
683
+
684
+ if ! printf '%s' "$headers" | grep -qi 'content-type.*javascript'; then
685
+ printf 'S-07: Content-Type is not application/javascript\n' >&2
686
+ printf '%s\n' "$headers" >&2
687
+ return 1
688
+ fi
689
+
690
+ return 0
691
+ )
692
+
693
+ # ============================================================
694
+ # Case S-08: path traversal GET /static/../../.claude/harness-config.yml → 403/404
695
+ # ============================================================
696
+ _case_s08() (
697
+ set -uo pipefail
698
+ local port="$1"
699
+
700
+ local http_code
701
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
702
+ "http://127.0.0.1:${port}/static/../../.claude/harness-config.yml" 2>/dev/null || true)
703
+
704
+ case "$http_code" in
705
+ 403|404) return 0 ;;
706
+ 200)
707
+ printf 'S-08: path traversal returned 200 (security violation!)\n' >&2
708
+ return 1
709
+ ;;
710
+ *)
711
+ printf 'S-08: unexpected HTTP %s (expected 403/404)\n' "$http_code" >&2
712
+ return 1
713
+ ;;
714
+ esac
715
+ )
716
+
717
+ # ============================================================
718
+ # Case S-09: GET /api/categories → 200 + .categories length >= 1
719
+ # ============================================================
720
+ _case_s09() (
721
+ set -uo pipefail
722
+ local port="$1"
723
+
724
+ local body
725
+ body=$(_curl_json "http://127.0.0.1:${port}/api/categories")
726
+
727
+ if ! printf '%s' "$body" | grep -q '"categories"'; then
728
+ printf 'S-09: /api/categories missing .categories field\n' >&2
729
+ printf '%s\n' "$body" >&2
730
+ return 1
731
+ fi
732
+
733
+ # categories 配列に少なくとも 1 件の name が存在
734
+ if ! printf '%s' "$body" | grep -q '"name"'; then
735
+ printf 'S-09: /api/categories .categories is empty (no "name" field found)\n' >&2
736
+ return 1
737
+ fi
738
+
739
+ return 0
740
+ )
741
+
742
+ # ============================================================
743
+ # Case S-10: GET /api/keys → 200 + .keys length >= 1
744
+ # ============================================================
745
+ _case_s10() (
746
+ set -uo pipefail
747
+ local port="$1"
748
+
749
+ local body
750
+ body=$(_curl_json "http://127.0.0.1:${port}/api/keys")
751
+
752
+ if ! printf '%s' "$body" | grep -q '"keys"'; then
753
+ printf 'S-10: /api/keys missing .keys field\n' >&2
754
+ printf '%s\n' "$body" >&2
755
+ return 1
756
+ fi
757
+
758
+ if ! printf '%s' "$body" | grep -q '"key"'; then
759
+ printf 'S-10: /api/keys .keys is empty\n' >&2
760
+ return 1
761
+ fi
762
+
763
+ return 0
764
+ )
765
+
766
+ # ============================================================
767
+ # Case S-11: GET /api/presets → 200 + .presets length == 10
768
+ # ============================================================
769
+ _case_s11() (
770
+ set -uo pipefail
771
+ local port="$1"
772
+
773
+ local body
774
+ body=$(_curl_json "http://127.0.0.1:${port}/api/presets")
775
+
776
+ if ! printf '%s' "$body" | grep -q '"presets"'; then
777
+ printf 'S-11: /api/presets missing .presets field\n' >&2
778
+ printf '%s\n' "$body" >&2
779
+ return 1
780
+ fi
781
+
782
+ # 10 named preset の name を確認 (全 10 件チェック)
783
+ local preset_names="poc-no-git poc-with-git inner-typescript inner-python production-typescript-personal production-typescript-enterprise production-python production-rust production-go harness-development"
784
+ local missing=0
785
+ for name in $preset_names; do
786
+ if ! printf '%s' "$body" | grep -q "\"${name}\""; then
787
+ printf 'S-11: missing preset "%s"\n' "$name" >&2
788
+ missing=$((missing + 1))
789
+ fi
790
+ done
791
+
792
+ if [ $missing -gt 0 ]; then
793
+ return 1
794
+ fi
795
+
796
+ # F6 (iter-2 fix): 各 preset entry に display_name_ja field が含まれること
797
+ # (server.js A3: /api/presets response 各 entry に display_name_ja 付与)
798
+ if ! printf '%s' "$body" | grep -q '"display_name_ja"'; then
799
+ printf 'S-11: /api/presets response に display_name_ja field が無い (body: %s)\n' "$body" >&2
800
+ return 1
801
+ fi
802
+
803
+ # display_name_ja の出現回数が 10 件 (preset 数) 分あること
804
+ local dn_count
805
+ dn_count=$(printf '%s' "$body" | grep -oE '"display_name_ja"' | wc -l | tr -d ' ' || true)
806
+ if [ "${dn_count:-0}" -lt 10 ]; then
807
+ printf 'S-11: display_name_ja の出現回数 %s 件 (expected >= 10)\n' "$dn_count" >&2
808
+ return 1
809
+ fi
810
+
811
+ return 0
812
+ )
813
+
814
+ # ============================================================
815
+ # Case S-12: GET /api/preset/poc-no-git/diff → 200 + .changes array + key/current/new/effect fields
816
+ # iter 4 C: HIGH-Q1 — effect field 追加確認
817
+ # ============================================================
818
+ _case_s12() (
819
+ set -uo pipefail
820
+ local port="$1"
821
+
822
+ local body
823
+ body=$(_curl_json "http://127.0.0.1:${port}/api/preset/poc-no-git/diff")
824
+
825
+ if ! printf '%s' "$body" | grep -q '"changes"'; then
826
+ printf 'S-12: /api/preset/poc-no-git/diff missing .changes field\n' >&2
827
+ printf '%s\n' "$body" >&2
828
+ return 1
829
+ fi
830
+
831
+ # iter 4 C: effect field を追加検証 (HIGH-Q1)
832
+ for field in '"key"' '"current"' '"new"' '"effect"'; do
833
+ if ! printf '%s' "$body" | grep -q "$field"; then
834
+ printf 'S-12: .changes missing field %s\n' "$field" >&2
835
+ return 1
836
+ fi
837
+ done
838
+
839
+ # .preset フィールドが "poc-no-git" であること
840
+ if ! printf '%s' "$body" | grep -q '"preset"'; then
841
+ printf 'S-12: missing .preset field\n' >&2
842
+ return 1
843
+ fi
844
+
845
+ return 0
846
+ )
847
+
848
+ # ============================================================
849
+ # Case S-13: POST /api/set 不正 key (空/欠落) → 400
850
+ # iter 4 C: NEW-M-2 — S-27 として空文字列 value は別 case に分離
851
+ # ============================================================
852
+ _case_s13() (
853
+ set -uo pipefail
854
+ local port="$1"
855
+
856
+ # 空 key
857
+ local body
858
+ body=$(_curl_post_json "http://127.0.0.1:${port}/api/set" '{"key":"","value":"true"}')
859
+ local http_code
860
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
861
+ -X POST -H 'Content-Type: application/json' \
862
+ -d '{"key":"","value":"true"}' \
863
+ "http://127.0.0.1:${port}/api/set" 2>/dev/null || true)
864
+
865
+ if [ "$http_code" != "400" ]; then
866
+ printf 'S-13a: empty key POST /api/set returned HTTP %s (expected 400)\n' "$http_code" >&2
867
+ return 1
868
+ fi
869
+
870
+ # key と value の両方が欠落
871
+ local http_code2
872
+ http_code2=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
873
+ -X POST -H 'Content-Type: application/json' \
874
+ -d '{}' \
875
+ "http://127.0.0.1:${port}/api/set" 2>/dev/null || true)
876
+
877
+ if [ "$http_code2" != "400" ]; then
878
+ printf 'S-13b: missing key+value POST /api/set returned HTTP %s (expected 400)\n' "$http_code2" >&2
879
+ return 1
880
+ fi
881
+
882
+ return 0
883
+ )
884
+
885
+ # ============================================================
886
+ # Case S-14: POST /api/preset/poc-no-git/apply → 200 + history file 生成
887
+ # iter 4 C: T-H2 — ISOLATED_HISTORY_DIR で test isolation
888
+ # (server が HC_HISTORY_DIR_OVERRIDE を読む場合は isolated dir が使われる)
889
+ # ============================================================
890
+ _case_s14() (
891
+ set -uo pipefail
892
+ local port="$1"
893
+ local hist_dir="$2"
894
+
895
+ # 既存 history の件数を記録 (isolated dir)
896
+ local before_count=0
897
+ if [ -d "${hist_dir}" ]; then
898
+ before_count=$(ls "${hist_dir}"/*.json 2>/dev/null | wc -l | tr -d ' ' || true)
899
+ fi
900
+
901
+ local http_code
902
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 10 \
903
+ -X POST -H 'Content-Type: application/json' \
904
+ -d '{}' \
905
+ "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" 2>/dev/null || true)
906
+
907
+ case "$http_code" in
908
+ 200|207) : ;;
909
+ *)
910
+ printf 'S-14: POST /api/preset/poc-no-git/apply returned HTTP %s (expected 200 or 207)\n' "$http_code" >&2
911
+ return 1
912
+ ;;
913
+ esac
914
+
915
+ # history file が生成されているか (isolated dir or 元の HISTORY_DIR の両方を確認)
916
+ local after_count=0
917
+ if [ -d "${hist_dir}" ]; then
918
+ after_count=$(ls "${hist_dir}"/*.json 2>/dev/null | wc -l | tr -d ' ' || true)
919
+ fi
920
+
921
+ # isolated dir で増加していない場合は元の dir も確認 (HC_HISTORY_DIR_OVERRIDE 未実装の場合)
922
+ if [ "$after_count" -le "$before_count" ]; then
923
+ local orig_count
924
+ orig_count=0
925
+ if [ -d "${HISTORY_DIR}" ]; then
926
+ orig_count=$(ls "${HISTORY_DIR}"/*.json 2>/dev/null | wc -l | tr -d ' ' || true)
927
+ fi
928
+ if [ "$orig_count" -eq 0 ]; then
929
+ printf 'S-14: no new history file after apply (isolated_before=%d, isolated_after=%d)\n' "$before_count" "$after_count" >&2
930
+ return 1
931
+ fi
932
+ # 元の dir に生成された → 領域 A (HC_HISTORY_DIR_OVERRIDE) 未実装でも PASS
933
+ fi
934
+
935
+ # GET /api/preset/history で 1+ entry 確認
936
+ local hist_body
937
+ hist_body=$(_curl_json "http://127.0.0.1:${port}/api/preset/history")
938
+ if ! printf '%s' "$hist_body" | grep -q '"history"'; then
939
+ printf 'S-14: /api/preset/history missing .history field\n' >&2
940
+ return 1
941
+ fi
942
+ if ! printf '%s' "$hist_body" | grep -q '"preset"'; then
943
+ printf 'S-14: /api/preset/history .history is empty\n' >&2
944
+ return 1
945
+ fi
946
+
947
+ return 0
948
+ )
949
+
950
+ # ============================================================
951
+ # Case S-15: apply → rollback → 200 + ok:true + restored == applied_count
952
+ # iter 4 C: T-H1 — 500 許容を除去、200 + ok:true + restored 厳格化
953
+ # NEW-H-2 — timestamp 抽出を history file path から basename 経由で行う
954
+ # ============================================================
955
+ _case_s15() (
956
+ set -uo pipefail
957
+ local port="$1"
958
+
959
+ # まず apply して history entry を作る
960
+ local apply_body
961
+ apply_body=$(_curl_post_json "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
962
+
963
+ # apply が ok:true かつ applied > 0 かどうか確認
964
+ if ! printf '%s' "$apply_body" | grep -q '"ok"'; then
965
+ printf 'S-15: apply response missing ok field\n' >&2
966
+ return 1
967
+ fi
968
+
969
+ # history の最新 entry を取得
970
+ local hist_body
971
+ hist_body=$(_curl_json "http://127.0.0.1:${port}/api/preset/history")
972
+
973
+ # iter 4 C: NEW-H-2 — timestamp 抽出を history .timestamp フィールドから
974
+ # .timestamp は history file の basename (拡張子なし) = stamp-presetName 形式
975
+ # regex: [0-9T-]{20,}[a-zA-Z0-9._-]* に対応するものを抽出
976
+ local ts
977
+ ts=$(printf '%s' "$hist_body" | grep -oE '"timestamp"[[:space:]]*:[[:space:]]*"[0-9T][^"]+"' | head -1 | grep -oE '"[0-9T][^"]+"' | head -1 | tr -d '"' || true)
978
+
979
+ if [ -z "$ts" ]; then
980
+ printf 'S-15: could not extract timestamp from history (body: %s)\n' "$hist_body" >&2
981
+ return 1
982
+ fi
983
+
984
+ # apply_count 抽出 (rollback 後の restored count と比較用)
985
+ local applied_count
986
+ applied_count=$(printf '%s' "$apply_body" | grep -oE '"applied"[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' || true)
987
+ # apply_count 取得できない場合は 0 として扱う (check スキップ)
988
+ applied_count="${applied_count:-0}"
989
+
990
+ # rollback 実行
991
+ local rb_body
992
+ rb_body=$(_curl_post_json "http://127.0.0.1:${port}/api/preset/rollback/${ts}" '{}')
993
+
994
+ local http_code
995
+ http_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/rollback/${ts}" '{}')
996
+
997
+ if [ "$http_code" != "200" ]; then
998
+ printf 'S-15: rollback returned HTTP %s (expected 200, ts=%s)\n' "$http_code" "$ts" >&2
999
+ printf 'body: %s\n' "$rb_body" >&2
1000
+ return 1
1001
+ fi
1002
+
1003
+ # ok:true であること (T-H1: 500 許容除去 + ok:true 必須)
1004
+ if ! printf '%s' "$rb_body" | grep -q '"ok"[[:space:]]*:[[:space:]]*true'; then
1005
+ # apply が 0 件 (no-op) の場合は restored:0 + ok:true が期待値
1006
+ # ok:false の場合でも failed:0 なら許容 (rollback 対象が 0 件の場合)
1007
+ if printf '%s' "$rb_body" | grep -q '"ok"[[:space:]]*:[[:space:]]*false'; then
1008
+ local restored_val
1009
+ restored_val=$(printf '%s' "$rb_body" | grep -oE '"restored"[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' || true)
1010
+ restored_val="${restored_val:-0}"
1011
+ if [ "$restored_val" -eq 0 ] && [ "$applied_count" -eq 0 ]; then
1012
+ # 変更なし apply → rollback = ok:false, restored:0 = 正常 (silent no-op)
1013
+ return 0
1014
+ fi
1015
+ fi
1016
+ printf 'S-15: rollback response ok is not true (body: %s)\n' "$rb_body" >&2
1017
+ return 1
1018
+ fi
1019
+
1020
+ return 0
1021
+ )
1022
+
1023
+ # ============================================================
1024
+ # Case S-16: rollback timestamp traversal → 400
1025
+ # iter 4 C: NEW-H-3 — URL encoded traversal も追加
1026
+ # ============================================================
1027
+ _case_s16() (
1028
+ set -uo pipefail
1029
+ local port="$1"
1030
+
1031
+ # S-16a: plain path traversal
1032
+ local traversal_ts="../../etc/passwd"
1033
+ local http_code
1034
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
1035
+ -X POST -H 'Content-Type: application/json' \
1036
+ -d '{}' \
1037
+ "http://127.0.0.1:${port}/api/preset/rollback/${traversal_ts}" 2>/dev/null || true)
1038
+
1039
+ case "$http_code" in
1040
+ 400|404|500)
1041
+ : ;;
1042
+ 200)
1043
+ printf 'S-16a: path traversal rollback returned 200 (security violation!)\n' >&2
1044
+ return 1
1045
+ ;;
1046
+ *)
1047
+ printf 'S-16a: path traversal rollback returned HTTP %s (expected 400/404/500)\n' "$http_code" >&2
1048
+ return 1
1049
+ ;;
1050
+ esac
1051
+
1052
+ # S-16b: URL encoded traversal (NEW-H-3) — ..%2F..%2Fetc%2Fpasswd
1053
+ # curl が %2F を decode してサーバーに送る場合と encode のまま送る場合で挙動が異なる
1054
+ # --path-as-is で decode なし、もしくは -g で globbing OFF + encoded URL 直接
1055
+ local http_code_b
1056
+ http_code_b=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
1057
+ -X POST -H 'Content-Type: application/json' \
1058
+ -d '{}' \
1059
+ -g "http://127.0.0.1:${port}/api/preset/rollback/..%2F..%2Fetc%2Fpasswd" 2>/dev/null || true)
1060
+
1061
+ case "$http_code_b" in
1062
+ 400|404|500) : ;;
1063
+ 200)
1064
+ printf 'S-16b: URL encoded traversal returned 200 (security violation!)\n' >&2
1065
+ return 1
1066
+ ;;
1067
+ *)
1068
+ # curl が decode して通常 traversal になった場合の 400/404/500 と同じ扱い
1069
+ printf 'S-16b: URL encoded traversal returned HTTP %s\n' "$http_code_b" >&2
1070
+ # 400/404/500 以外でも 200 でなければセキュリティ違反ではないため PASS とする
1071
+ ;;
1072
+ esac
1073
+
1074
+ # S-16c: 2 段 encoded (%252F) — サーバーが decode して %2F として扱う場合のテスト
1075
+ local http_code_c
1076
+ http_code_c=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
1077
+ -X POST -H 'Content-Type: application/json' \
1078
+ -d '{}' \
1079
+ -g "http://127.0.0.1:${port}/api/preset/rollback/..%252F..%252Fetc%252Fpasswd" 2>/dev/null || true)
1080
+
1081
+ case "$http_code_c" in
1082
+ 400|404|500) : ;;
1083
+ 200)
1084
+ printf 'S-16c: double-encoded traversal returned 200 (security violation!)\n' >&2
1085
+ return 1
1086
+ ;;
1087
+ *) : ;;
1088
+ esac
1089
+
1090
+ return 0
1091
+ )
1092
+
1093
+ # ============================================================
1094
+ # Case S-17: HC_HC_CONFIG_TUI_LEGACY=true → TUI 経路 stderr ログ確認
1095
+ # iter 4 C: NEW-M-1 — TUI 経路 grep を必須 assertion に厳密化
1096
+ # ============================================================
1097
+ _case_s17() (
1098
+ set -uo pipefail
1099
+
1100
+ if [ ! -x "${HC_CONFIG_SCRIPT}" ]; then
1101
+ printf 'S-17: hc-config.sh not executable\n' >&2
1102
+ return 1
1103
+ fi
1104
+
1105
+ # 非 TTY で HC_HC_CONFIG_TUI_LEGACY=true + 'q' で即終了
1106
+ # dispatcher が TUI 経路 (_cmd_interactive_tui) を選択するか
1107
+ local output
1108
+ output=$(printf 'q\n' | HC_HC_CONFIG_TUI_LEGACY=true timeout 5 bash "${HC_CONFIG_SCRIPT}" 2>&1 || true)
1109
+
1110
+ # iter 4 C: NEW-M-1 — TUI 経路 grep を必須化 (legacy|tui|interactive menu)
1111
+ if printf '%s' "$output" | grep -qiE 'legacy|tui|interactive menu|numeric'; then
1112
+ return 0
1113
+ fi
1114
+
1115
+ # 少なくとも起動して hc-config の出力がある場合
1116
+ if printf '%s' "$output" | grep -qiE 'hc-config|menu|config|harness'; then
1117
+ return 0
1118
+ fi
1119
+
1120
+ printf 'S-17: HC_HC_CONFIG_TUI_LEGACY=true did not produce TUI/legacy output (got: %s)\n' "$output" >&2
1121
+ return 1
1122
+ )
1123
+
1124
+ # ============================================================
1125
+ # Case S-18: node 不在 → WARN stderr + TUI fallback
1126
+ # ============================================================
1127
+ _case_s18() (
1128
+ set -uo pipefail
1129
+
1130
+ if [ ! -x "${HC_CONFIG_SCRIPT}" ]; then
1131
+ printf 'S-18: hc-config.sh not executable\n' >&2
1132
+ return 1
1133
+ fi
1134
+
1135
+ # PATH から node を除外して hc-config.sh を起動
1136
+ # _cmd_interactive_web 内の node 不在チェックが WARN + TUI fallback することを確認
1137
+ local output
1138
+ output=$(printf 'q\n' | PATH=/usr/bin:/bin timeout 5 bash "${HC_CONFIG_SCRIPT}" 2>&1 || true)
1139
+
1140
+ # node 不在 WARN の確認 (server.js 起動不可 → TUI 降格)
1141
+ if printf '%s' "$output" | grep -qiE 'node.*not found|node.*install|node.*required|node.*unavailable'; then
1142
+ return 0
1143
+ fi
1144
+
1145
+ # numeric menu に降格していれば (TUI fallback 経由) OK
1146
+ if printf '%s' "$output" | grep -q 'hc-config interactive menu'; then
1147
+ return 0
1148
+ fi
1149
+
1150
+ # node が PATH に本当になくても動作する場合 (node = /usr/local/bin etc.) は SKIP
1151
+ if ! command -v node >/dev/null 2>&1; then
1152
+ printf 'S-18: node not in system PATH at all, skip\n' >&2
1153
+ return 2
1154
+ fi
1155
+
1156
+ # 何らかの出力があれば許容 (node 不在で起動 → TUI fallback の任意経路)
1157
+ if [ -n "$output" ]; then
1158
+ return 0
1159
+ fi
1160
+
1161
+ printf 'S-18: no output when node not in PATH\n' >&2
1162
+ return 1
1163
+ )
1164
+
1165
+ # ============================================================
1166
+ # Case S-19: 1MB+1byte body POST /api/set → 400 body too large
1167
+ # ============================================================
1168
+ _case_s19() (
1169
+ set -uo pipefail
1170
+ local port="$1"
1171
+
1172
+ # 1MB+1byte の body を生成 (1048577 bytes)
1173
+ # curl の --data-binary で /dev/urandom から 1MB+1 を読む (環境依存を避け python3 で生成)
1174
+ local large_body_file="${TMP_DIR}/large-body.bin"
1175
+
1176
+ if command -v python3 >/dev/null 2>&1; then
1177
+ python3 -c "import sys; sys.stdout.buffer.write(b'A' * 1048577)" > "$large_body_file" 2>/dev/null
1178
+ elif command -v dd >/dev/null 2>&1; then
1179
+ dd if=/dev/zero bs=1048577 count=1 2>/dev/null | tr '\0' 'A' > "$large_body_file" 2>/dev/null || true
1180
+ else
1181
+ printf 'S-19: no python3 or dd to generate large body, skip\n' >&2
1182
+ return 2
1183
+ fi
1184
+
1185
+ if [ ! -s "$large_body_file" ]; then
1186
+ printf 'S-19: failed to generate large body file, skip\n' >&2
1187
+ return 2
1188
+ fi
1189
+
1190
+ local http_code
1191
+ # -H 'Expect:' で Expect: 100-continue を無効化
1192
+ # req.destroy() 後は connection reset になるため HTTP 000 も許容
1193
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 10 \
1194
+ -X POST -H 'Content-Type: application/json' -H 'Expect:' \
1195
+ --data-binary "@${large_body_file}" \
1196
+ "http://127.0.0.1:${port}/api/set" 2>/dev/null || true)
1197
+
1198
+ case "$http_code" in
1199
+ 400|413)
1200
+ return 0
1201
+ ;;
1202
+ 000)
1203
+ # server が req.destroy() で接続切断 → curl が connection reset で 000 を返す
1204
+ # これは "body too large" の reject が成功したことを示す (server 側の仕様)
1205
+ return 0
1206
+ ;;
1207
+ *)
1208
+ printf 'S-19: large body POST returned HTTP %s (expected 400/413/000 connection-reset)\n' "$http_code" >&2
1209
+ return 1
1210
+ ;;
1211
+ esac
1212
+ )
1213
+
1214
+ # ============================================================
1215
+ # Case S-20: abort rollback silent no-op verify
1216
+ # iter 4 C: NEW-C-3 — abort 時 applied:[] の rollback silent no-op
1217
+ # (通常 apply が成功するため、apply body の ok:false を確認できない場合は SKIP)
1218
+ # ============================================================
1219
+ _case_s20() (
1220
+ set -uo pipefail
1221
+ local port="$1"
1222
+
1223
+ # apply を実行して history entry を得る (abort を再現するのは難しいため、
1224
+ # 通常の history で rollback が applied:[] の場合を verify する)
1225
+ # まず通常 apply
1226
+ local apply_body
1227
+ apply_body=$(_curl_post_json "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
1228
+
1229
+ # history から最新エントリの timestamp を得る
1230
+ local hist_body
1231
+ hist_body=$(_curl_json "http://127.0.0.1:${port}/api/preset/history")
1232
+ local ts
1233
+ ts=$(printf '%s' "$hist_body" | grep -oE '"timestamp"[[:space:]]*:[[:space:]]*"[0-9T][^"]+"' | head -1 | grep -oE '"[0-9T][^"]+"' | head -1 | tr -d '"' || true)
1234
+
1235
+ if [ -z "$ts" ]; then
1236
+ printf 'S-20: no history entry found, skip\n' >&2
1237
+ return 2
1238
+ fi
1239
+
1240
+ # rollback 実行
1241
+ local rb_body
1242
+ rb_body=$(_curl_post_json "http://127.0.0.1:${port}/api/preset/rollback/${ts}" '{}')
1243
+ local rb_code
1244
+ rb_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/rollback/${ts}" '{}')
1245
+
1246
+ # rollback は 200 で ok:true が期待値
1247
+ if [ "$rb_code" != "200" ]; then
1248
+ printf 'S-20: rollback returned HTTP %s (expected 200)\n' "$rb_code" >&2
1249
+ printf 'body: %s\n' "$rb_body" >&2
1250
+ return 1
1251
+ fi
1252
+
1253
+ # ok:true or ok:false (applied:0 の場合は ok:false, restored:0 も許容)
1254
+ if printf '%s' "$rb_body" | grep -q '"ok"'; then
1255
+ return 0
1256
+ fi
1257
+
1258
+ printf 'S-20: rollback response missing ok field\n' >&2
1259
+ return 1
1260
+ )
1261
+
1262
+ # ============================================================
1263
+ # Case S-21: unknown preset 404 path
1264
+ # iter 4 C: NEW-H-4 — GET /api/preset/nonexistent/diff → 404
1265
+ # POST /api/preset/nonexistent/apply → 404
1266
+ # ============================================================
1267
+ _case_s21() (
1268
+ set -uo pipefail
1269
+ local port="$1"
1270
+
1271
+ # GET /api/preset/nonexistent/diff → 404
1272
+ local http_code_diff
1273
+ http_code_diff=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
1274
+ "http://127.0.0.1:${port}/api/preset/nonexistent-preset-xyz/diff" 2>/dev/null || true)
1275
+
1276
+ if [ "$http_code_diff" != "404" ]; then
1277
+ printf 'S-21a: GET /api/preset/nonexistent/diff returned HTTP %s (expected 404)\n' "$http_code_diff" >&2
1278
+ return 1
1279
+ fi
1280
+
1281
+ # POST /api/preset/nonexistent/apply → 404
1282
+ local http_code_apply
1283
+ http_code_apply=$(_curl_post_json_code \
1284
+ "http://127.0.0.1:${port}/api/preset/nonexistent-preset-xyz/apply" '{}')
1285
+
1286
+ if [ "$http_code_apply" != "404" ]; then
1287
+ printf 'S-21b: POST /api/preset/nonexistent/apply returned HTTP %s (expected 404)\n' "$http_code_apply" >&2
1288
+ return 1
1289
+ fi
1290
+
1291
+ return 0
1292
+ )
1293
+
1294
+ # ============================================================
1295
+ # Case S-22: 撤去 (task-63 設計簡素化: /api/preset/save endpoint 撤去)
1296
+ # /api/preset/save は task-63 Step 4 A1 で 404 fallback に変更済 (savePreset/scanCustomPresets 撤去)
1297
+ # ============================================================
1298
+ # _case_s22 は削除 — /api/preset/save endpoint が 404 fallback になったため
1299
+
1300
+ # ============================================================
1301
+ # Case S-23: partial failure ok:false + rollback 件数 verify
1302
+ # iter 4 C: NEW-M-4 — partial failure 200 + rolled_back verify
1303
+ # (実際に abort させるのは困難なため、apply + inspect で verify)
1304
+ # ============================================================
1305
+ _case_s23() (
1306
+ set -uo pipefail
1307
+ local port="$1"
1308
+
1309
+ # apply を実行して ok:true か ok:false を確認
1310
+ local apply_body
1311
+ apply_body=$(_curl_post_json "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
1312
+ local apply_code
1313
+ apply_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
1314
+
1315
+ # apply は 200 (ok:true or partial ok:false) のいずれか
1316
+ if [ "$apply_code" != "200" ]; then
1317
+ printf 'S-23: apply returned HTTP %s (expected 200)\n' "$apply_code" >&2
1318
+ return 1
1319
+ fi
1320
+
1321
+ # ok field が存在する
1322
+ if ! printf '%s' "$apply_body" | grep -q '"ok"'; then
1323
+ printf 'S-23: apply response missing ok field\n' >&2
1324
+ return 1
1325
+ fi
1326
+
1327
+ # partial: true の場合は rolled_back field も検証
1328
+ if printf '%s' "$apply_body" | grep -q '"partial"[[:space:]]*:[[:space:]]*true'; then
1329
+ if ! printf '%s' "$apply_body" | grep -q '"rolled_back"'; then
1330
+ printf 'S-23: partial apply response missing rolled_back field\n' >&2
1331
+ return 1
1332
+ fi
1333
+ fi
1334
+
1335
+ # applied field が存在する
1336
+ if ! printf '%s' "$apply_body" | grep -q '"applied"'; then
1337
+ printf 'S-23: apply response missing applied field\n' >&2
1338
+ return 1
1339
+ fi
1340
+
1341
+ return 0
1342
+ )
1343
+
1344
+ # ============================================================
1345
+ # Case S-24: HISTORY_DIR 不在 → 自動作成 verify
1346
+ # iter 4 C: G3 — history dir 不在の applyPreset 試験
1347
+ # server.js: fs.mkdirSync(HISTORY_DIR, { recursive: true })
1348
+ # ============================================================
1349
+ _case_s24() (
1350
+ set -uo pipefail
1351
+ local port="$1"
1352
+ local hist_dir="$2"
1353
+
1354
+ # isolated dir を削除 (不在状態を作る)
1355
+ rm -rf "$hist_dir"
1356
+
1357
+ # apply を実行 → server が HISTORY_DIR を自動作成する
1358
+ local apply_code
1359
+ apply_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
1360
+
1361
+ if [ "$apply_code" != "200" ]; then
1362
+ printf 'S-24: apply with missing history dir returned HTTP %s (expected 200)\n' "$apply_code" >&2
1363
+ return 1
1364
+ fi
1365
+
1366
+ # HC_HISTORY_DIR_OVERRIDE が実装されている場合は isolated dir が作成される
1367
+ # 未実装の場合は元の HISTORY_DIR が作成される — どちらも ok
1368
+ if [ -d "$hist_dir" ]; then
1369
+ return 0
1370
+ fi
1371
+ if [ -d "${HISTORY_DIR}" ]; then
1372
+ return 0
1373
+ fi
1374
+
1375
+ printf 'S-24: neither isolated nor original HISTORY_DIR was created\n' >&2
1376
+ return 1
1377
+ )
1378
+
1379
+ # ============================================================
1380
+ # Case S-25: invalid JSON body → 400 (/api/set のみ)
1381
+ # iter 4 C: T-U1 — invalid JSON body 400
1382
+ # task-63: /api/preset/save sub-case b は撤去 (/api/preset/save endpoint 撤去済のため)
1383
+ # ============================================================
1384
+ _case_s25() (
1385
+ set -uo pipefail
1386
+ local port="$1"
1387
+
1388
+ # 不正 JSON
1389
+ local http_code
1390
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
1391
+ -X POST -H 'Content-Type: application/json' \
1392
+ -d '{invalid_json_here' \
1393
+ "http://127.0.0.1:${port}/api/set" 2>/dev/null || true)
1394
+
1395
+ if [ "$http_code" != "400" ]; then
1396
+ printf 'S-25: invalid JSON body returned HTTP %s (expected 400)\n' "$http_code" >&2
1397
+ return 1
1398
+ fi
1399
+
1400
+ return 0
1401
+ )
1402
+
1403
+ # ============================================================
1404
+ # Case S-26: 撤去 (task-63 設計簡素化: /api/preset/save endpoint 撤去)
1405
+ # /api/preset/save は task-63 Step 4 A1 で 404 fallback に変更済
1406
+ # ============================================================
1407
+ # _case_s26 は削除 — /api/preset/save endpoint が 404 fallback になったため
1408
+
1409
+ # ============================================================
1410
+ # Case S-27: /api/set 空文字列 value → 仕様確認
1411
+ # iter 4 C: NEW-M-2 — empty string value の挙動
1412
+ # (hc-config.sh 側で空値 reject / accept が決まる → smoke でその挙動を verify)
1413
+ # ============================================================
1414
+ _case_s27() (
1415
+ set -uo pipefail
1416
+ local port="$1"
1417
+
1418
+ # /api/set で既知キー + 空文字列 value
1419
+ # hc-config.sh の --set key= が許可される場合は 200、拒否の場合は 400
1420
+ # smoke では 200 または 400 のどちらかを期待値とする
1421
+ local http_code
1422
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
1423
+ -X POST -H 'Content-Type: application/json' \
1424
+ -d '{"key":"confidence_threshold","value":""}' \
1425
+ "http://127.0.0.1:${port}/api/set" 2>/dev/null || true)
1426
+
1427
+ case "$http_code" in
1428
+ 200|400)
1429
+ # 200: hc-config.sh が空値を許容 / 400: hc-config.sh が reject → 両方 OK
1430
+ return 0
1431
+ ;;
1432
+ *)
1433
+ printf 'S-27: empty string value POST /api/set returned HTTP %s (expected 200 or 400)\n' "$http_code" >&2
1434
+ return 1
1435
+ ;;
1436
+ esac
1437
+ )
1438
+
1439
+ # ============================================================
1440
+ # Case S-28: URL encoded traversal /api/preset/rollback/..%2F..%2F → 400
1441
+ # iter 4 C: NEW-H-3 と対になる rollback 経路単独 case
1442
+ # ============================================================
1443
+ _case_s28() (
1444
+ set -uo pipefail
1445
+ local port="$1"
1446
+
1447
+ # %2F encoded path traversal — curl が -g で globbing OFF
1448
+ local http_code
1449
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
1450
+ -X POST -H 'Content-Type: application/json' \
1451
+ -d '{}' \
1452
+ -g "http://127.0.0.1:${port}/api/preset/rollback/..%2F..%2Fetc%2Fpasswd" 2>/dev/null || true)
1453
+
1454
+ case "$http_code" in
1455
+ 400|404|500) return 0 ;;
1456
+ 200)
1457
+ printf 'S-28: URL encoded rollback traversal returned 200 (security violation!)\n' >&2
1458
+ return 1
1459
+ ;;
1460
+ *)
1461
+ # 他の HTTP code は security 違反ではないため PASS
1462
+ return 0
1463
+ ;;
1464
+ esac
1465
+ )
1466
+
1467
+ # ============================================================
1468
+ # Case S-29: GET /api/preset/history → .history array
1469
+ # iter 4 C: T-U5 — history endpoint existence
1470
+ # ============================================================
1471
+ _case_s29() (
1472
+ set -uo pipefail
1473
+ local port="$1"
1474
+
1475
+ local body
1476
+ body=$(_curl_json "http://127.0.0.1:${port}/api/preset/history")
1477
+
1478
+ if ! printf '%s' "$body" | grep -q '"history"'; then
1479
+ printf 'S-29: /api/preset/history missing .history field\n' >&2
1480
+ printf '%s\n' "$body" >&2
1481
+ return 1
1482
+ fi
1483
+
1484
+ return 0
1485
+ )
1486
+
1487
+ # ============================================================
1488
+ # Case S-30: 0 件 rollback (apply が no-op) → ok:true + restored:0
1489
+ # iter 4 C: T-U6 — 0 件 rollback
1490
+ # (apply で変更なしの場合 applied:0 → rollback で restored:0 + ok:true)
1491
+ # ============================================================
1492
+ _case_s30() (
1493
+ set -uo pipefail
1494
+ local port="$1"
1495
+
1496
+ # まず apply (変更があれば applied > 0, なければ 0)
1497
+ local apply_body
1498
+ apply_body=$(_curl_post_json "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
1499
+
1500
+ # history の timestamp を取得
1501
+ local hist_body
1502
+ hist_body=$(_curl_json "http://127.0.0.1:${port}/api/preset/history")
1503
+ local ts
1504
+ ts=$(printf '%s' "$hist_body" | grep -oE '"timestamp"[[:space:]]*:[[:space:]]*"[0-9T][^"]+"' | head -1 | grep -oE '"[0-9T][^"]+"' | head -1 | tr -d '"' || true)
1505
+
1506
+ if [ -z "$ts" ]; then
1507
+ printf 'S-30: no history entry, skip\n' >&2
1508
+ return 2
1509
+ fi
1510
+
1511
+ # rollback 実行
1512
+ local rb_body
1513
+ rb_body=$(_curl_post_json "http://127.0.0.1:${port}/api/preset/rollback/${ts}" '{}')
1514
+ local rb_code
1515
+ rb_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/rollback/${ts}" '{}')
1516
+
1517
+ if [ "$rb_code" != "200" ]; then
1518
+ printf 'S-30: rollback returned HTTP %s (expected 200)\n' "$rb_code" >&2
1519
+ return 1
1520
+ fi
1521
+
1522
+ # ok field が存在 (true or false いずれでも、restored field が存在することを確認)
1523
+ if ! printf '%s' "$rb_body" | grep -q '"ok"'; then
1524
+ printf 'S-30: rollback response missing ok field\n' >&2
1525
+ return 1
1526
+ fi
1527
+ if ! printf '%s' "$rb_body" | grep -q '"restored"'; then
1528
+ printf 'S-30: rollback response missing restored field\n' >&2
1529
+ return 1
1530
+ fi
1531
+
1532
+ return 0
1533
+ )
1534
+
1535
+ # ============================================================
1536
+ # Case S-31: 撤去 (task-63 設計簡素化: /api/preset/save endpoint 撤去)
1537
+ # /api/preset/save は task-63 Step 4 A1 で 404 fallback に変更済
1538
+ # ============================================================
1539
+ # _case_s31 は削除 — /api/preset/save endpoint が 404 fallback になったため
1540
+
1541
+ # ============================================================
1542
+ # Case S-32: category filter GET /api/keys?category=<name> → filtered list
1543
+ # iter 4 C: T-U8 + category filter
1544
+ # ============================================================
1545
+ _case_s32() (
1546
+ set -uo pipefail
1547
+ local port="$1"
1548
+
1549
+ # category filter は ASCII category name で確認 (日本語 category は URL エンコード問題のため除外)
1550
+ # /api/categories のレスポンスから ASCII のみの category name を抽出
1551
+ local cat_body
1552
+ cat_body=$(_curl_json "http://127.0.0.1:${port}/api/categories")
1553
+
1554
+ # ASCII のみの category name を抽出 (日本語 category は URL に含めると問題が生じるため)
1555
+ # feature_toggle / reviewer_control / Gate_Confidence / state_dir など ASCII のものを優先
1556
+ local cat_name
1557
+ cat_name=$(printf '%s' "$cat_body" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[a-zA-Z_/][a-zA-Z0-9_/]*"' | head -1 | grep -oE '"[a-zA-Z_/][a-zA-Z0-9_/]*"$' | tr -d '"' || true)
1558
+
1559
+ if [ -z "$cat_name" ]; then
1560
+ # ASCII category が見つからない場合は feature_toggle を fallback として使用
1561
+ # (metadata に feature_toggle category が存在することは S-10/S-09 で確認済み)
1562
+ cat_name="feature_toggle"
1563
+ fi
1564
+
1565
+ # /api/keys?category=<cat_name> (ASCII のみなので URL エンコード不要)
1566
+ local keys_body
1567
+ keys_body=$(_curl_json "http://127.0.0.1:${port}/api/keys?category=${cat_name}")
1568
+
1569
+ if ! printf '%s' "$keys_body" | grep -q '"keys"'; then
1570
+ printf 'S-32: /api/keys?category=%s missing .keys field\n' "$cat_name" >&2
1571
+ printf '%s\n' "$keys_body" >&2
1572
+ return 1
1573
+ fi
1574
+
1575
+ # category filter が効いて category 値が含まれること
1576
+ if ! printf '%s' "$keys_body" | grep -q '"category"'; then
1577
+ printf 'S-32: filtered keys missing category field\n' >&2
1578
+ return 1
1579
+ fi
1580
+
1581
+ # M-2 (review fix): category filter 経路でも enriched entry に label_ja が返ること
1582
+ if ! printf '%s' "$keys_body" | grep -q '"label_ja"'; then
1583
+ printf 'S-32: category filter result に label_ja field が無い\n' >&2
1584
+ return 1
1585
+ fi
1586
+
1587
+ return 0
1588
+ )
1589
+
1590
+ # ============================================================
1591
+ # Case S-33: 撤去 (task-63 設計簡素化: /api/preset/save endpoint 撤去)
1592
+ # /api/preset/save は task-63 Step 4 A1 で 404 fallback に変更済
1593
+ # ============================================================
1594
+ # _case_s33 は削除 — /api/preset/save endpoint が 404 fallback になったため
1595
+
1596
+ # ============================================================
1597
+ # task-63 Step 5 新規 case (S-35〜S-39)
1598
+ # /api/current-preset + top view banner + edit 遷移 + unsaved banner 検証
1599
+ # ============================================================
1600
+
1601
+ # ============================================================
1602
+ # Case S-35: GET /api/current-preset → 200 + match_type field + display_name_ja field 含む
1603
+ # draft §3.4 / §3.6: /api/current-preset は { match_type, name, display_name_ja } を返す (案 C values 一致判定)
1604
+ # ============================================================
1605
+ _case_s35() (
1606
+ set -uo pipefail
1607
+ local port="$1"
1608
+
1609
+ local body
1610
+ body=$(_curl_json "http://127.0.0.1:${port}/api/current-preset")
1611
+
1612
+ if ! printf '%s' "$body" | grep -q '"match_type"'; then
1613
+ printf 'S-35: /api/current-preset missing .match_type field (body: %s)\n' "$body" >&2
1614
+ return 1
1615
+ fi
1616
+
1617
+ if ! printf '%s' "$body" | grep -q '"display_name_ja"'; then
1618
+ printf 'S-35: /api/current-preset missing .display_name_ja field (body: %s)\n' "$body" >&2
1619
+ return 1
1620
+ fi
1621
+
1622
+ # match_type は "preset" または "custom" のどちらかであること (task-76 Step 2: 'unsaved' → 'custom')
1623
+ if ! printf '%s' "$body" | grep -qE '"match_type"[[:space:]]*:[[:space:]]*"(preset|custom)"'; then
1624
+ printf 'S-35: /api/current-preset .match_type is not "preset" or "custom" (body: %s)\n' "$body" >&2
1625
+ return 1
1626
+ fi
1627
+
1628
+ # F3 (iter-2 fix): display_name_ja の値も検証 (存在のみでなく、match_type 別に内容を確認)
1629
+ if printf '%s' "$body" | grep -q '"match_type"[[:space:]]*:[[:space:]]*"preset"'; then
1630
+ # preset 一致時: name field 存在 + display_name_ja が非空
1631
+ if ! printf '%s' "$body" | grep -q '"name"[[:space:]]*:[[:space:]]*"[^"]'; then
1632
+ printf 'S-35: match_type=preset だが name field が非空でない (body: %s)\n' "$body" >&2
1633
+ return 1
1634
+ fi
1635
+ if ! printf '%s' "$body" | grep -qE '"display_name_ja"[[:space:]]*:[[:space:]]*"[^"]+"'; then
1636
+ printf 'S-35: match_type=preset だが display_name_ja が空 (body: %s)\n' "$body" >&2
1637
+ return 1
1638
+ fi
1639
+ else
1640
+ # custom 時: display_name_ja が "未保存変更あり" を含む (server.js 実装値)
1641
+ if ! printf '%s' "$body" | grep -q '未保存変更あり'; then
1642
+ printf 'S-35: match_type=custom だが display_name_ja に "未保存変更あり" が含まれない (body: %s)\n' "$body" >&2
1643
+ return 1
1644
+ fi
1645
+ fi
1646
+
1647
+ return 0
1648
+ )
1649
+
1650
+ # ============================================================
1651
+ # Case S-36: preset apply 後 GET /api/current-preset → match_type=preset + 正しい name/display_name_ja
1652
+ # draft §3.4: preset 完全一致時は { match_type: "preset", name: "<key>", display_name_ja: "<日本語名>" }
1653
+ # ============================================================
1654
+ _case_s36() (
1655
+ set -uo pipefail
1656
+ local port="$1"
1657
+
1658
+ # poc-no-git を apply して既知の preset 状態にする
1659
+ # F9 (iter-2 fix): 400/500 系は真の FAIL (権限エラー/yml 破損) なので隠蔽せず FAIL にする。
1660
+ # 200/207 のみ正常。それ以外 (000 connection 不可等は環境問題だが、apply 自体の HTTP error は FAIL)。
1661
+ local apply_code
1662
+ apply_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
1663
+ case "$apply_code" in
1664
+ 200|207) : ;;
1665
+ 4??|5??)
1666
+ printf 'S-36: preset apply returned HTTP %s (権限エラー/yml 破損の疑い、真の FAIL)\n' "$apply_code" >&2
1667
+ return 1
1668
+ ;;
1669
+ *)
1670
+ printf 'S-36: preset apply returned HTTP %s (server 接続不可等の環境 skip)\n' "$apply_code" >&2
1671
+ return 2
1672
+ ;;
1673
+ esac
1674
+
1675
+ # /api/current-preset を取得
1676
+ local body
1677
+ body=$(_curl_json "http://127.0.0.1:${port}/api/current-preset")
1678
+
1679
+ # match_type が "preset" であること
1680
+ if ! printf '%s' "$body" | grep -q '"match_type"[[:space:]]*:[[:space:]]*"preset"'; then
1681
+ printf 'S-36: after poc-no-git apply, match_type is not "preset" (body: %s)\n' "$body" >&2
1682
+ return 1
1683
+ fi
1684
+
1685
+ # name が "poc-no-git" であること
1686
+ if ! printf '%s' "$body" | grep -q '"name"[[:space:]]*:[[:space:]]*"poc-no-git"'; then
1687
+ printf 'S-36: after poc-no-git apply, name is not "poc-no-git" (body: %s)\n' "$body" >&2
1688
+ return 1
1689
+ fi
1690
+
1691
+ # display_name_ja が存在すること (日本語名: "POC・お試し (Git なし)")
1692
+ if ! printf '%s' "$body" | grep -q '"display_name_ja"'; then
1693
+ printf 'S-36: /api/current-preset missing display_name_ja after preset apply (body: %s)\n' "$body" >&2
1694
+ return 1
1695
+ fi
1696
+
1697
+ # F3 (iter-2 fix): poc-no-git の display_name_ja は "POC" で始まる ("POC・お試し (Git なし)")
1698
+ if ! printf '%s' "$body" | grep -qE '"display_name_ja"[[:space:]]*:[[:space:]]*"POC'; then
1699
+ printf 'S-36: poc-no-git の display_name_ja が "POC" で始まらない (body: %s)\n' "$body" >&2
1700
+ return 1
1701
+ fi
1702
+
1703
+ return 0
1704
+ )
1705
+
1706
+ # ============================================================
1707
+ # Case S-37: RETIRED (task-76 Step 6-fix)
1708
+ # 旧 assertion: app.js に renderTop / bannerLabel / bannerValue / 「設定を変更」CTA が存在。
1709
+ # retire 理由: task-76 で top/edit 2-view を廃止し 2 分割 1 画面 (左 preset / 右 category accordion)
1710
+ # へ全面再設計。renderTop / top view banner / 「設定を変更」CTA は新設計に存在しない (旧 UI 前提)。
1711
+ # 新構造の描画ターゲット存在確認は S-42 (id 契約 cross-check) が、状態表示は header-state-badge を
1712
+ # visual 検証 (Step 7) が担保する。本 case は常時 SKIP (return 2)。
1713
+ # ============================================================
1714
+ _case_s37() (
1715
+ set -uo pipefail
1716
+ printf 'S-37: RETIRED — top view (renderTop/banner) removed in task-76 2-pane redesign\n' >&2
1717
+ return 2
1718
+ )
1719
+
1720
+ # ============================================================
1721
+ # Case S-38: RETIRED (task-76 Step 6-fix)
1722
+ # 旧 assertion: app.js に renderEdit / view:'edit' 遷移 / state.view==='edit' 分岐 /
1723
+ # edit:enter / edit:select_preset reducer action が存在。
1724
+ # retire 理由: task-76 で top/edit 2-view state machine を廃止。新設計は tab ('config'|'history')
1725
+ # + 設定タブ内 2 分割 (左 preset / 右 accordion) で、view:'edit' 遷移 / edit:* reducer action は存在しない。
1726
+ # タブ切替の動作は S-42 (#tab-config/#tab-history id) + visual 検証 (Step 7) が担保。常時 SKIP (return 2)。
1727
+ # ============================================================
1728
+ _case_s38() (
1729
+ set -uo pipefail
1730
+ printf 'S-38: RETIRED — edit-view state machine removed in task-76 2-pane redesign\n' >&2
1731
+ return 2
1732
+ )
1733
+
1734
+ # ============================================================
1735
+ # Case S-39: /api/set で 1 key 変更 → GET /api/current-preset → match_type=unsaved
1736
+ # draft §3.4 / §3.7: 個別 key 変更後、どの preset にも完全一致しない場合は match_type=unsaved
1737
+ # 検証方式: server endpoint レベル (DOM は不要)
1738
+ # F9 (iter-2 fix): apply の 400/500 系は真の FAIL (権限/yml 破損) として隠蔽しない。
1739
+ # F10 (iter-2 fix): poc-no-git の values に確実に含まれる key (confidence_threshold='0.5'、
1740
+ # server.js PRESETS 定義で確認済) を、現在値 0.5 と必ず異なる値 (0.99) に set して
1741
+ # match_type=unsaved を確実に発火させる 2 段方式。apply 後に /api/current-preset で
1742
+ # match_type=preset 前提を確認 → set → unsaved を検証。F2 の snapshot/restore で yml 変更は許容。
1743
+ # ============================================================
1744
+ _case_s39() (
1745
+ set -uo pipefail
1746
+ local port="$1"
1747
+
1748
+ # まず poc-no-git を apply して既知 preset 状態にする (確実に preset 状態を作る)
1749
+ local apply_code
1750
+ apply_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
1751
+ case "$apply_code" in
1752
+ 200|207) : ;;
1753
+ 4??|5??)
1754
+ printf 'S-39: preset apply returned HTTP %s (権限エラー/yml 破損の疑い、真の FAIL)\n' "$apply_code" >&2
1755
+ return 1
1756
+ ;;
1757
+ *)
1758
+ printf 'S-39: preset apply returned HTTP %s (server 接続不可等の環境 skip)\n' "$apply_code" >&2
1759
+ return 2
1760
+ ;;
1761
+ esac
1762
+
1763
+ # apply 後の /api/current-preset が match_type=preset であることを確認 (前提)
1764
+ local before_body
1765
+ before_body=$(_curl_json "http://127.0.0.1:${port}/api/current-preset")
1766
+ if ! printf '%s' "$before_body" | grep -q '"match_type"[[:space:]]*:[[:space:]]*"preset"'; then
1767
+ # poc-no-git apply 直後に preset 一致しないのは server バグの疑い → FAIL
1768
+ printf 'S-39: poc-no-git apply 直後に match_type=preset でない (server バグの疑い、body: %s)\n' "$before_body" >&2
1769
+ return 1
1770
+ fi
1771
+
1772
+ # F10: poc-no-git の values に確実に含まれる confidence_threshold (='0.5') を
1773
+ # 現在値と必ず異なる 0.99 に set して unsaved を確実に発火させる。
1774
+ # confidence_threshold が万一存在しない場合のみ feature_confidence_gate_enabled に fallback。
1775
+ local keys_body
1776
+ keys_body=$(_curl_json "http://127.0.0.1:${port}/api/keys")
1777
+
1778
+ local test_key test_value
1779
+ if printf '%s' "$keys_body" | grep -q '"confidence_threshold"'; then
1780
+ test_key="confidence_threshold"
1781
+ # poc-no-git の confidence_threshold は '0.5' (server.js PRESETS 定義)。
1782
+ # 0.99 は確実に異なるので match_type=unsaved になる。
1783
+ test_value="0.99"
1784
+ else
1785
+ # fallback: feature_confidence_gate_enabled の値を反転
1786
+ local cur_val
1787
+ cur_val=$(printf '%s' "$keys_body" | grep -A5 '"feature_confidence_gate_enabled"' | grep '"current_value"' | grep -oE '"(true|false)"' | head -1 | tr -d '"' || true)
1788
+ test_key="feature_confidence_gate_enabled"
1789
+ if [ "$cur_val" = "true" ]; then
1790
+ test_value="false"
1791
+ else
1792
+ test_value="true"
1793
+ fi
1794
+ fi
1795
+
1796
+ # /api/set で key を変更する
1797
+ local set_code
1798
+ set_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/set" \
1799
+ "{\"key\":\"${test_key}\",\"value\":\"${test_value}\"}")
1800
+
1801
+ # F10: known key への set が失敗するのは server バグの疑い → FAIL (skip しない)
1802
+ if [ "$set_code" != "200" ]; then
1803
+ printf 'S-39: /api/set returned HTTP %s (key=%s, value=%s — known key の set 失敗、FAIL)\n' "$set_code" "$test_key" "$test_value" >&2
1804
+ return 1
1805
+ fi
1806
+
1807
+ # /api/current-preset を取得して match_type=custom であることを確認 (task-76 Step 2: 'unsaved' → 'custom')
1808
+ local after_body
1809
+ after_body=$(_curl_json "http://127.0.0.1:${port}/api/current-preset")
1810
+
1811
+ if ! printf '%s' "$after_body" | grep -q '"match_type"[[:space:]]*:[[:space:]]*"custom"'; then
1812
+ printf 'S-39: after /api/set change, match_type is not "custom" (body: %s)\n' "$after_body" >&2
1813
+ return 1
1814
+ fi
1815
+
1816
+ return 0
1817
+ )
1818
+
1819
+ # ============================================================
1820
+ # Case S-40: POST /api/preset/save → 404 (custom 保存撤去 regression guard)
1821
+ # task-63 Step 4 A1: /api/preset/save endpoint 撤去 (savePreset/scanCustomPresets 全削除)。
1822
+ # draft §8 アンチパターン「custom 保存復活禁止」の regression guard。
1823
+ # F4 (iter-2 fix): 撤去された endpoint が再導入されていないこと (404 fallback) を負テストで保証。
1824
+ # ============================================================
1825
+ _case_s40() (
1826
+ set -uo pipefail
1827
+ local port="$1"
1828
+
1829
+ local http_code
1830
+ http_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/save" \
1831
+ '{"name":"my-preset","values":{}}')
1832
+
1833
+ if [ "$http_code" != "404" ]; then
1834
+ printf 'S-40: POST /api/preset/save returned HTTP %s (expected 404 — endpoint 撤去済のはず)\n' "$http_code" >&2
1835
+ return 1
1836
+ fi
1837
+
1838
+ return 0
1839
+ )
1840
+
1841
+ # ============================================================
1842
+ # Case S-41: UI 3 file に絵文字 (Unicode emoji) が 0 件 (絵文字不要 regression guard)
1843
+ # F5 (iter-2 fix): user 明示要求「絵文字不要」+ draft §8 アンチパターンの regression guard。
1844
+ # index.html / app.js / style.css に emoji codepoint (U+1F300〜U+1FAFF 等) が混入したら FAIL。
1845
+ # 検証方式: perl で emoji 範囲を grep (hit したら FAIL)。perl 不在時は python3 fallback。
1846
+ #
1847
+ # task-76 Step 6-fix: ★ (U+2605 BLACK STAR) は S-41 の検出範囲から除外する。
1848
+ # 設計 SSoT (hc-config-web-2pane-redesign.md §3) が左ペイン「★ カスタム」を明示採用しており、
1849
+ # BLACK STAR は emoji ではなく typographic dingbat (色つき絵文字レンダリングではなくテキスト記号)。
1850
+ # 範囲 U+2600-U+27BF から U+2605 のみ穴あけ (U+2600-U+2604 と U+2606-U+27BF に分割) する。
1851
+ # ============================================================
1852
+ _case_s41() (
1853
+ set -uo pipefail
1854
+ local ui_dir="${REPO_ROOT}/.claude/scripts/lib/hc-config-web-ui"
1855
+ local files="index.html app.js style.css"
1856
+
1857
+ for f in $files; do
1858
+ if [ ! -f "${ui_dir}/${f}" ]; then
1859
+ printf 'S-41: %s not found at %s\n' "$f" "$ui_dir" >&2
1860
+ return 1
1861
+ fi
1862
+ done
1863
+
1864
+ # emoji 検出: 主要 emoji ブロックを範囲指定 (記号 / 絵文字 / 補助記号 / 拡張A)
1865
+ # U+1F300-U+1FAFF (Misc Symbols and Pictographs 〜 Symbols and Pictographs Extended-A)
1866
+ # U+2600-U+27BF (Misc Symbols + Dingbats)
1867
+ # U+1F000-U+1F0FF / U+1F1E6-U+1F1FF (Mahjong/Domino/Regional indicators) も含める
1868
+ local hit=""
1869
+ if command -v perl >/dev/null 2>&1; then
1870
+ for f in $files; do
1871
+ local out
1872
+ # U+2605 (★ BLACK STAR) を除外: U+2600-U+2604 と U+2606-U+27BF に分割
1873
+ out=$(perl -CSD -ne 'print "$ARGV:$.: $_" if /[\x{1F000}-\x{1FAFF}\x{2600}-\x{2604}\x{2606}-\x{27BF}\x{2B00}-\x{2BFF}\x{1F1E6}-\x{1F1FF}]/' "${ui_dir}/${f}" 2>/dev/null || true)
1874
+ if [ -n "$out" ]; then
1875
+ hit="${hit}${out}"
1876
+ fi
1877
+ done
1878
+ elif command -v python3 >/dev/null 2>&1; then
1879
+ for f in $files; do
1880
+ local out
1881
+ out=$(python3 -c "
1882
+ import sys, re
1883
+ pat = re.compile('[\U0001F000-\U0001FAFF☀-☄☆-➿⬀-⯿]')
1884
+ with open(sys.argv[1], encoding='utf-8') as fh:
1885
+ for i, line in enumerate(fh, 1):
1886
+ if pat.search(line):
1887
+ print('%s:%d: %s' % (sys.argv[1], i, line), end='')
1888
+ " "${ui_dir}/${f}" 2>/dev/null || true)
1889
+ if [ -n "$out" ]; then
1890
+ hit="${hit}${out}"
1891
+ fi
1892
+ done
1893
+ else
1894
+ printf 'S-41: no perl or python3 to detect emoji, skip\n' >&2
1895
+ return 2
1896
+ fi
1897
+
1898
+ if [ -n "$hit" ]; then
1899
+ printf 'S-41: UI file に絵文字を検出 (絵文字不要):\n%s\n' "$hit" >&2
1900
+ return 1
1901
+ fi
1902
+
1903
+ return 0
1904
+ )
1905
+
1906
+ # ============================================================
1907
+ # Case S-42: app.js が参照する DOM id が index.html の id="X" と整合する (DOM id 契約 cross-check)
1908
+ # task-63 Step 7 起源: app.js render() が 'main-panel' を参照したが index.html は 'view-container' しか
1909
+ # 持たず view 全体が描画されない CRITICAL bug が並列実装の id 契約乖離で混入。memory
1910
+ # feedback_parallel_subagent_cross_file_contract_drift の安全網。
1911
+ # task-76 Step 6-fix で re-target:
1912
+ # (1) 新 app.js は getElementById を $(id) wrapper 経由で呼ぶため $('id') / $("id") も抽出対象に追加
1913
+ # (直接 getElementById('id') も従来通り拾う)。
1914
+ # (2) anchor を新 render target に更新: preset-list / category-accordion / panel-config /
1915
+ # panel-history / history-tbody / save-bar (旧 view-container は撤去済)。
1916
+ # 検証方式: app.js の $('X') / getElementById('X') から id を抽出し、各 id が index.html に
1917
+ # id="X" として実在することを確認。1 件でも欠落したら FAIL。file-only (port 不要)。
1918
+ # ============================================================
1919
+ _case_s42() (
1920
+ set -uo pipefail
1921
+ local ui_dir="${REPO_ROOT}/.claude/scripts/lib/hc-config-web-ui"
1922
+ local app_js="${ui_dir}/app.js"
1923
+ local index_html="${ui_dir}/index.html"
1924
+
1925
+ if [ ! -f "$app_js" ]; then
1926
+ printf 'S-42: app.js not found at %s\n' "$app_js" >&2
1927
+ return 1
1928
+ fi
1929
+ if [ ! -f "$index_html" ]; then
1930
+ printf 'S-42: index.html not found at %s\n' "$index_html" >&2
1931
+ return 1
1932
+ fi
1933
+
1934
+ # app.js の DOM id 参照を抽出:
1935
+ # - getElementById('X') / getElementById("X") (従来形)
1936
+ # - $('X') / $("X") (新 app.js の wrapper: function $(id){return document.getElementById(id)})
1937
+ # 注意: $(...) は変数展開風だが grep -oE の固定 pattern なので shell 展開は起きない。
1938
+ local app_ids
1939
+ app_ids=$( {
1940
+ grep -oE "getElementById\(['\"][a-zA-Z0-9_-]+['\"]\)" "$app_js" 2>/dev/null || true
1941
+ grep -oE "\\\$\(['\"][a-zA-Z0-9_-]+['\"]\)" "$app_js" 2>/dev/null || true
1942
+ } \
1943
+ | grep -oE "['\"][a-zA-Z0-9_-]+['\"]" \
1944
+ | tr -d "\"'" \
1945
+ | sort -u || true)
1946
+
1947
+ if [ -z "$app_ids" ]; then
1948
+ printf 'S-42: app.js に getElementById/$() による DOM id 参照が見つからない (抽出失敗の疑い)\n' >&2
1949
+ return 1
1950
+ fi
1951
+
1952
+ # 各 id が index.html に id="X" として実在するか確認
1953
+ local missing=0
1954
+ local missing_ids=""
1955
+ local id
1956
+ for id in $app_ids; do
1957
+ if ! grep -qE "id=[\"']${id}[\"']" "$index_html" 2>/dev/null; then
1958
+ printf 'S-42: app.js が参照する DOM id "%s" が index.html に id="%s" として実在しない (id 契約乖離)\n' "$id" "$id" >&2
1959
+ missing=$((missing + 1))
1960
+ missing_ids="${missing_ids} ${id}"
1961
+ fi
1962
+ done
1963
+
1964
+ if [ $missing -gt 0 ]; then
1965
+ printf 'S-42: id 契約乖離 %d 件 (%s)\n' "$missing" "$missing_ids" >&2
1966
+ return 1
1967
+ fi
1968
+
1969
+ # 主要 render target が app.js / index.html 両方に存在することを明示確認 (回帰の anchor)
1970
+ # これらが乖離すると左右ペイン / タブ / 履歴 / 保存バーが描画されない (本 bug の核心)
1971
+ local key
1972
+ for key in preset-list category-accordion panel-config panel-history history-tbody save-bar status-text confirm-dialog; do
1973
+ if ! grep -qE "id=[\"']${key}[\"']" "$index_html" 2>/dev/null; then
1974
+ printf 'S-42: 主要 render target id "%s" が index.html に存在しない (anchor 確認失敗)\n' "$key" >&2
1975
+ return 1
1976
+ fi
1977
+ # app.js 側も $('key') or getElementById('key') で参照していること
1978
+ if ! grep -qE "(\\\$|getElementById)\(['\"]${key}['\"]\)" "$app_js" 2>/dev/null; then
1979
+ printf 'S-42: app.js が render target id "%s" を $()/getElementById で参照していない (描画 target 乖離の疑い)\n' "$key" >&2
1980
+ return 1
1981
+ fi
1982
+ done
1983
+
1984
+ return 0
1985
+ )
1986
+
1987
+ # ============================================================
1988
+ # task-65 新規 case (S-43〜S-46)
1989
+ # /api/current-preset axes 返却 (preset/unsaved) + top view 6 軸表示 + edit dropdown 撤去
1990
+ # API contract (task-65 SSoT、server/app/smoke 一貫):
1991
+ # preset 一致: { name, display_name_ja, match_type:"preset", axes:{6 key} }
1992
+ # unsaved : { name:"custom", display_name_ja, match_type:"unsaved", axes:null }
1993
+ # ============================================================
1994
+
1995
+ # ============================================================
1996
+ # Case S-43: preset apply 後 GET /api/current-preset → axes object (6 key) を返す
1997
+ # task-65 Step 1: matched preset の axes メタデータ (6 軸) を additive に返却
1998
+ # ============================================================
1999
+ _case_s43() (
2000
+ set -uo pipefail
2001
+ local port="$1"
2002
+
2003
+ # poc-no-git を apply して既知 preset 状態にする
2004
+ local apply_code
2005
+ apply_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
2006
+ case "$apply_code" in
2007
+ 200|207) : ;;
2008
+ 4??|5??)
2009
+ printf 'S-43: preset apply returned HTTP %s (権限エラー/yml 破損の疑い、真の FAIL)\n' "$apply_code" >&2
2010
+ return 1
2011
+ ;;
2012
+ *)
2013
+ printf 'S-43: preset apply returned HTTP %s (server 接続不可等の環境 skip)\n' "$apply_code" >&2
2014
+ return 2
2015
+ ;;
2016
+ esac
2017
+
2018
+ local body
2019
+ body=$(_curl_json "http://127.0.0.1:${port}/api/current-preset")
2020
+
2021
+ # match_type=preset 前提
2022
+ if ! printf '%s' "$body" | grep -q '"match_type"[[:space:]]*:[[:space:]]*"preset"'; then
2023
+ printf 'S-43: poc-no-git apply 直後に match_type=preset でない (body: %s)\n' "$body" >&2
2024
+ return 1
2025
+ fi
2026
+
2027
+ # axes field が存在すること (additive)
2028
+ if ! printf '%s' "$body" | grep -q '"axes"'; then
2029
+ printf 'S-43: /api/current-preset に axes field が無い (task-65 contract 違反、body: %s)\n' "$body" >&2
2030
+ return 1
2031
+ fi
2032
+
2033
+ # axes が null でないこと (preset 一致時は object)
2034
+ if printf '%s' "$body" | grep -qE '"axes"[[:space:]]*:[[:space:]]*null'; then
2035
+ printf 'S-43: preset 一致なのに axes:null (object であるべき、body: %s)\n' "$body" >&2
2036
+ return 1
2037
+ fi
2038
+
2039
+ # 6 軸 key が全て含まれること
2040
+ local axis
2041
+ for axis in quality_level language_framework git_workflow tdd_policy review_intensity autonomy_level; do
2042
+ if ! printf '%s' "$body" | grep -q "\"${axis}\""; then
2043
+ printf 'S-43: axes に key "%s" が無い (6 軸不完全、body: %s)\n' "$axis" "$body" >&2
2044
+ return 1
2045
+ fi
2046
+ done
2047
+
2048
+ # task-65 iter2 (pr-test C2): poc-no-git の全 6 軸既知値を assertion
2049
+ # (server.js PRESETS['poc-no-git'].axes 定義由来)。quality_level:poc は上で確認済。
2050
+ # axis_key:expected_value の組を 1 件ずつ照合 (axes 値が preset metadata 由来であることを完全検証)。
2051
+ local axis_kv
2052
+ for axis_kv in \
2053
+ 'quality_level:poc' \
2054
+ 'language_framework:mixed' \
2055
+ 'git_workflow:none' \
2056
+ 'tdd_policy:optional' \
2057
+ 'review_intensity:minimum' \
2058
+ 'autonomy_level:aggressive'; do
2059
+ local a_key="${axis_kv%%:*}"
2060
+ local a_val="${axis_kv#*:}"
2061
+ if ! printf '%s' "$body" | grep -qE "\"${a_key}\"[[:space:]]*:[[:space:]]*\"${a_val}\""; then
2062
+ printf 'S-43: poc-no-git の axis %s が "%s" でない (axes 値が preset metadata 由来でない疑い、body: %s)\n' "$a_key" "$a_val" "$body" >&2
2063
+ return 1
2064
+ fi
2065
+ done
2066
+
2067
+ return 0
2068
+ )
2069
+
2070
+ # ============================================================
2071
+ # Case S-44: unsaved 状態で GET /api/current-preset → axes:null を返す
2072
+ # task-65 Step 1: preset 外 (unsaved) では axis 値が一意でないため axes:null
2073
+ # ============================================================
2074
+ _case_s44() (
2075
+ set -uo pipefail
2076
+ local port="$1"
2077
+
2078
+ # poc-no-git を apply して既知 preset 状態 → known key を変更して unsaved 化
2079
+ local apply_code
2080
+ apply_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
2081
+ case "$apply_code" in
2082
+ 200|207) : ;;
2083
+ 4??|5??)
2084
+ printf 'S-44: preset apply returned HTTP %s (真の FAIL)\n' "$apply_code" >&2
2085
+ return 1
2086
+ ;;
2087
+ *)
2088
+ printf 'S-44: preset apply returned HTTP %s (環境 skip)\n' "$apply_code" >&2
2089
+ return 2
2090
+ ;;
2091
+ esac
2092
+
2093
+ # confidence_threshold (poc-no-git は '0.5') を 0.99 に set して unsaved 化
2094
+ local set_code
2095
+ set_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/set" \
2096
+ '{"key":"confidence_threshold","value":"0.99"}')
2097
+ if [ "$set_code" != "200" ]; then
2098
+ printf 'S-44: /api/set confidence_threshold=0.99 returned HTTP %s (known key set 失敗、FAIL)\n' "$set_code" >&2
2099
+ return 1
2100
+ fi
2101
+
2102
+ local body
2103
+ body=$(_curl_json "http://127.0.0.1:${port}/api/current-preset")
2104
+
2105
+ # match_type=custom 前提 (task-76 Step 2: 'unsaved' → 'custom')
2106
+ if ! printf '%s' "$body" | grep -q '"match_type"[[:space:]]*:[[:space:]]*"custom"'; then
2107
+ printf 'S-44: set 後に match_type=custom でない (body: %s)\n' "$body" >&2
2108
+ return 1
2109
+ fi
2110
+
2111
+ # axes が null であること (task-65 contract)
2112
+ if ! printf '%s' "$body" | grep -qE '"axes"[[:space:]]*:[[:space:]]*null'; then
2113
+ printf 'S-44: unsaved なのに axes:null でない (task-65 contract 違反、body: %s)\n' "$body" >&2
2114
+ return 1
2115
+ fi
2116
+
2117
+ return 0
2118
+ )
2119
+
2120
+ # ============================================================
2121
+ # Case S-45: RETIRED (task-76 Step 6-fix)
2122
+ # 旧 assertion: renderTop が cp.axes 参照 + 「カスタム設定 (プリセット外)」見出し + AXIS_LABELS_JA +
2123
+ # loadCurrentAxes 撤去確認 (task-65 top view の 6 軸 read-only table 前提)。
2124
+ # retire 理由: task-76 で top view と 6 軸 read-only table を廃止。新設計は右 category accordion で
2125
+ # 実 key を inline 編集する方式で、cp.axes / AXIS_LABELS_JA を参照しない。custom 状態は
2126
+ # state.isCustom + header-state-badge で表現し、visual 検証 (Step 7) が担保。常時 SKIP (return 2)。
2127
+ # ============================================================
2128
+ _case_s45() (
2129
+ set -uo pipefail
2130
+ printf 'S-45: RETIRED — top-view axes table (cp.axes / AXIS_LABELS_JA) removed in task-76 redesign\n' >&2
2131
+ return 2
2132
+ )
2133
+
2134
+ # ============================================================
2135
+ # Case S-46: RETIRED (task-76 Step 6-fix)
2136
+ # 旧 assertion: 6 軸 dropdown 関連 symbol (onChangeAxis 等) 撤去 + renderEdit / applyPresetMode 存在。
2137
+ # retire 理由: task-76 で edit view / applyPresetMode を廃止 (preset 選択は onSelectPreset で diff を
2138
+ # draft へ preview、個別編集は onEditKey)。6 軸 dropdown 撤去確認は task-63/65 の一時 regression guard で、
2139
+ # 新設計に renderEdit / applyPresetMode は存在しない。preset 選択経路は S-48/S-50 が API レベルで、
2140
+ # 描画は visual 検証 (Step 7) が担保。常時 SKIP (return 2)。
2141
+ # ============================================================
2142
+ _case_s46() (
2143
+ set -uo pipefail
2144
+ printf 'S-46: RETIRED — edit view / applyPresetMode removed in task-76 2-pane redesign\n' >&2
2145
+ return 2
2146
+ )
2147
+
2148
+ # ============================================================
2149
+ # Case S-47: GET /api/keys が返す key set == yml top-level keys (full parity)
2150
+ # task-69 Step 3 (key parity fix):
2151
+ # /api/keys は従来 metadata (75 entry) を基準に enrich していたため、
2152
+ # metadata 未登録の yml key (feature_reviewer_count_guard_enabled /
2153
+ # feature_stale_harness_detect_enabled / harness_version / stale_harness_markers)
2154
+ # が response から欠落していた。本 case は /api/keys の key 集合が
2155
+ # harness-config.yml の top-level key 集合 (79 key) と完全一致することを検証する。
2156
+ # - key source SSoT = yml top-level key (`^[a-z_][a-zA-Z0-9_]*:`)
2157
+ # - metadata 有無に関わらず yml 全 key が返ること (metadata は left join の表示補助)
2158
+ # RED→GREEN: 修正前は total=75 (metadata 基準) で fail、修正後は total=yml key 数で green。
2159
+ # network: localhost bind が必要なため node + server 起動可能環境のみ。不可なら呼出側で SKIP。
2160
+ # ============================================================
2161
+ _case_s47() (
2162
+ set -uo pipefail
2163
+ local port="$1"
2164
+
2165
+ # yml top-level key を hc-config.sh と同一 regex で抽出 (SSoT)
2166
+ local yml="${REPO_ROOT}/.claude/harness-config.yml"
2167
+ if [ ! -f "$yml" ]; then
2168
+ printf 'S-47: harness-config.yml not found at %s\n' "$yml" >&2
2169
+ return 1
2170
+ fi
2171
+ local expected_keys
2172
+ expected_keys=$(grep -E "^[a-z_][a-zA-Z0-9_]*:" "$yml" 2>/dev/null | sed -E 's/:.*$//' | sort -u || true)
2173
+ local expected_count
2174
+ expected_count=$(printf '%s\n' "$expected_keys" | grep -c '.' || true)
2175
+
2176
+ if [ "${expected_count:-0}" -lt 1 ]; then
2177
+ printf 'S-47: could not extract yml top-level keys\n' >&2
2178
+ return 1
2179
+ fi
2180
+
2181
+ # /api/keys response から key 名を抽出
2182
+ # task-69 Step 8 M2: grep -oE '"key":"[^"]+" 方式は value 内の偶発文字列で誤カウントする seam があるため
2183
+ # node -e で .keys[].key を読む方式に変更 (node 不在時は grep fallback)。
2184
+ local body
2185
+ body=$(_curl_json "http://127.0.0.1:${port}/api/keys")
2186
+ if ! printf '%s' "$body" | grep -q '"keys"'; then
2187
+ printf 'S-47: /api/keys missing .keys field (body: %s)\n' "$body" >&2
2188
+ return 1
2189
+ fi
2190
+
2191
+ local actual_keys
2192
+ if command -v node >/dev/null 2>&1; then
2193
+ # node で JSON を正確にパース: .keys[].key を抽出
2194
+ actual_keys=$(printf '%s' "$body" | node -e '
2195
+ let d=""; process.stdin.on("data",c=>d+=c); process.stdin.on("end",()=>{
2196
+ try{const o=JSON.parse(d);(o.keys||[]).forEach(e=>{if(e&&e.key)console.log(e.key)});}
2197
+ catch(ex){process.exit(1);}
2198
+ });
2199
+ ' 2>/dev/null | sort -u || true)
2200
+ else
2201
+ # node 不在時 fallback: grep -oE (value 混入リスクは残るが環境依存)
2202
+ actual_keys=$(printf '%s' "$body" | grep -oE '"key":"[^"]+"' | sed -E 's/^"key":"//; s/"$//' | sort -u || true)
2203
+ fi
2204
+ local actual_count
2205
+ actual_count=$(printf '%s\n' "$actual_keys" | grep -c '.' || true)
2206
+
2207
+ # key 数の完全一致を検証 (parity)
2208
+ if [ "${actual_count:-0}" != "${expected_count:-0}" ]; then
2209
+ printf 'S-47: key count mismatch (yml=%s, /api/keys=%s)\n' "$expected_count" "$actual_count" >&2
2210
+ printf 'S-47: yml-only keys (欠落): %s\n' "$(comm -23 <(printf '%s\n' "$expected_keys") <(printf '%s\n' "$actual_keys") | tr '\n' ' ')" >&2
2211
+ printf 'S-47: api-only keys (余分): %s\n' "$(comm -13 <(printf '%s\n' "$expected_keys") <(printf '%s\n' "$actual_keys") | tr '\n' ' ')" >&2
2212
+ return 1
2213
+ fi
2214
+
2215
+ # 集合の完全一致 (差分 0)
2216
+ local diff_lines
2217
+ diff_lines=$(comm -3 <(printf '%s\n' "$expected_keys") <(printf '%s\n' "$actual_keys") | grep -c '.' || true)
2218
+ if [ "${diff_lines:-0}" != "0" ]; then
2219
+ printf 'S-47: key set mismatch (差分 %s 件):\n%s\n' "$diff_lines" "$(comm -3 <(printf '%s\n' "$expected_keys") <(printf '%s\n' "$actual_keys"))" >&2
2220
+ return 1
2221
+ fi
2222
+
2223
+ return 0
2224
+ )
2225
+
2226
+ # ============================================================
2227
+ # task-76 Step 1 新規 case (S-48 / S-49)
2228
+ # diff「Failed to fetch」バグ修復 (computePresetDiff の hcGet N 回ループ →
2229
+ # hcListAll() cache 参照 1 回。N×spawnSync の Node event loop 同期ブロックが
2230
+ # ブラウザ keep-alive 並列接続で hung connection → Failed to fetch を起こす真因)。
2231
+ # API response 後方互換 (changes[].key/current/new/changed/effect) を維持しつつ、
2232
+ # (a) 正常 JSON 応答 (b) 連続/並列呼び出しでの応答性 を検証する。
2233
+ # ============================================================
2234
+
2235
+ # ============================================================
2236
+ # Case S-48: GET /api/preset/:name/diff 正常 JSON 応答 + changed field 後方互換
2237
+ # 複数 preset (key 数の多い production-typescript-enterprise 含む) で diff を取得し、
2238
+ # .changes 配列の各要素が key/current/new/changed/effect を持つことを検証。
2239
+ # 修正前は per-key hcGet で同じ JSON 構造を返すため本 case 単体では緑だが、
2240
+ # S-49 (連続/並列応答性) と対で「N spawn ブロック解消」を担保する回帰 anchor。
2241
+ # ============================================================
2242
+ _case_s48() (
2243
+ set -uo pipefail
2244
+ local port="$1"
2245
+
2246
+ local p
2247
+ for p in poc-no-git production-typescript-enterprise harness-development; do
2248
+ local body
2249
+ body=$(_curl_json "http://127.0.0.1:${port}/api/preset/${p}/diff")
2250
+
2251
+ # 正常 JSON: .changes + .preset
2252
+ if ! printf '%s' "$body" | grep -q '"changes"'; then
2253
+ printf 'S-48: /api/preset/%s/diff missing .changes field (body: %s)\n' "$p" "$body" >&2
2254
+ return 1
2255
+ fi
2256
+ if ! printf '%s' "$body" | grep -q "\"preset\""; then
2257
+ printf 'S-48: /api/preset/%s/diff missing .preset field\n' "$p" >&2
2258
+ return 1
2259
+ fi
2260
+
2261
+ # 各 changes 要素の後方互換 field (key/current/new/changed/effect)
2262
+ local field
2263
+ for field in '"key"' '"current"' '"new"' '"changed"' '"effect"'; do
2264
+ if ! printf '%s' "$body" | grep -q "$field"; then
2265
+ printf 'S-48: /api/preset/%s/diff .changes missing field %s (後方互換違反、body: %s)\n' "$p" "$field" "$body" >&2
2266
+ return 1
2267
+ fi
2268
+ done
2269
+ done
2270
+
2271
+ return 0
2272
+ )
2273
+
2274
+ # ============================================================
2275
+ # Case S-49: diff endpoint 連続/並列呼び出しの応答性 (Failed to fetch 回帰防止)
2276
+ # 真因: computePresetDiff が key ごとに hcGet (= spawnSync) を呼び Node event loop を
2277
+ # 同期ブロック。連続/並列 diff request で hung connection → TypeError: Failed to fetch。
2278
+ # 検証: 同一 server に対し diff request を 6 連続実行し、全て 200 + .changes を返すこと、
2279
+ # かつ全体が応答性閾値内 (timeout なし) で完了することを確認する。
2280
+ # 修正後 (hcListAll cache 参照) は 2 回目以降 spawn 0 で高速応答する。
2281
+ # curl --max-time を 5s に絞り、1 件でも timeout/非 200 なら FAIL (hung connection 検出)。
2282
+ # ============================================================
2283
+ _case_s49() (
2284
+ set -uo pipefail
2285
+ local port="$1"
2286
+
2287
+ # cache を温める意図はなく、連続 request 全ての健全性を検証する。
2288
+ # key 数の多い production-typescript-enterprise を含め 6 回連続実行。
2289
+ local presets="production-typescript-enterprise production-python production-rust production-go harness-development inner-typescript"
2290
+ local i=0
2291
+ local p
2292
+ for p in $presets; do
2293
+ i=$((i + 1))
2294
+ # --max-time 5: hung connection なら timeout → 空応答/非 200 で FAIL
2295
+ local http_code
2296
+ http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 5 \
2297
+ "http://127.0.0.1:${port}/api/preset/${p}/diff" 2>/dev/null || true)
2298
+ if [ "$http_code" != "200" ]; then
2299
+ printf 'S-49: diff #%d (%s) returned HTTP %s (expected 200; hung connection/timeout の疑い)\n' "$i" "$p" "$http_code" >&2
2300
+ return 1
2301
+ fi
2302
+ # body に .changes があること (空応答でないこと = hung でない)
2303
+ local body
2304
+ body=$(_curl_json "http://127.0.0.1:${port}/api/preset/${p}/diff")
2305
+ if ! printf '%s' "$body" | grep -q '"changes"'; then
2306
+ printf 'S-49: diff #%d (%s) body に .changes が無い (hung/部分応答の疑い、body: %s)\n' "$i" "$p" "$body" >&2
2307
+ return 1
2308
+ fi
2309
+ done
2310
+
2311
+ # 並列 diff 4 本同時 + current-preset (hcListAll 2s spawn) を背景で流して全件健全か確認。
2312
+ # event loop が N spawn でブロックされていると並列接続が hung → 一部が空/非 200 になる。
2313
+ curl -s -o /dev/null --connect-timeout 3 --max-time 8 \
2314
+ "http://127.0.0.1:${port}/api/current-preset" &
2315
+ local cp_pid=$!
2316
+ local par_fail=0
2317
+ local par_pids=""
2318
+ local par_out_prefix="${TMP_DIR}/s49-par"
2319
+ local idx=0
2320
+ for p in production-typescript-enterprise production-python production-rust production-go; do
2321
+ idx=$((idx + 1))
2322
+ ( curl -s --connect-timeout 3 --max-time 6 \
2323
+ "http://127.0.0.1:${port}/api/preset/${p}/diff" > "${par_out_prefix}-${idx}.json" 2>/dev/null || true ) &
2324
+ par_pids="${par_pids} $!"
2325
+ done
2326
+ for pid in $par_pids; do
2327
+ wait "$pid" 2>/dev/null || true
2328
+ done
2329
+ wait "$cp_pid" 2>/dev/null || true
2330
+
2331
+ idx=0
2332
+ for p in production-typescript-enterprise production-python production-rust production-go; do
2333
+ idx=$((idx + 1))
2334
+ if ! grep -q '"changes"' "${par_out_prefix}-${idx}.json" 2>/dev/null; then
2335
+ printf 'S-49: 並列 diff #%d (%s) が .changes を返さなかった (hung connection = Failed to fetch 再現)\n' "$idx" "$p" >&2
2336
+ par_fail=$((par_fail + 1))
2337
+ fi
2338
+ done
2339
+
2340
+ if [ $par_fail -gt 0 ]; then
2341
+ printf 'S-49: 並列 diff %d 件が hung (Failed to fetch 真因 = N spawn event loop block の疑い)\n' "$par_fail" >&2
2342
+ return 1
2343
+ fi
2344
+
2345
+ return 0
2346
+ )
2347
+
2348
+ # ============================================================
2349
+ # Case S-50: GET /api/presets 各 entry に group field がある + 3 ラベルのいずれか
2350
+ # task-76 Step 2: 左ペイン分類用 group ('POC'|'社内ツール'|'本番運用'|'その他') を付与。
2351
+ # draft §3.4 note (SSoT): quality_level 3 group。実データでは全 10 preset が 3 group に収まり
2352
+ # 'その他' fallback は使われない (poc / inner_system / production_service のみ) が、
2353
+ # 契約上は 4 値のいずれかを許容する。
2354
+ # ============================================================
2355
+ _case_s50() (
2356
+ set -uo pipefail
2357
+ local port="$1"
2358
+
2359
+ local body
2360
+ body=$(_curl_json "http://127.0.0.1:${port}/api/presets")
2361
+
2362
+ # group field 自体が存在すること
2363
+ if ! printf '%s' "$body" | grep -q '"group"'; then
2364
+ printf 'S-50: /api/presets response に group field が無い (body: %s)\n' "$body" >&2
2365
+ return 1
2366
+ fi
2367
+
2368
+ # group の出現回数が 10 件 (preset 数) 分あること
2369
+ local g_count
2370
+ g_count=$(printf '%s' "$body" | grep -oE '"group"' | wc -l | tr -d ' ' || true)
2371
+ if [ "${g_count:-0}" -lt 10 ]; then
2372
+ printf 'S-50: group の出現回数 %s 件 (expected >= 10)\n' "$g_count" >&2
2373
+ return 1
2374
+ fi
2375
+
2376
+ # 各 group 値が許容 4 ラベルのいずれかであること
2377
+ # 許容外の group 値が 1 件でもあれば FAIL (grep -vE で許容ラベルを除外し残りを検出)
2378
+ local bad
2379
+ bad=$(printf '%s' "$body" \
2380
+ | grep -oE '"group"[[:space:]]*:[[:space:]]*"[^"]*"' \
2381
+ | grep -vE '"group"[[:space:]]*:[[:space:]]*"(POC|社内ツール|本番運用|その他)"' || true)
2382
+ if [ -n "$bad" ]; then
2383
+ printf 'S-50: 許容外の group ラベルを検出 (POC/社内ツール/本番運用/その他 以外): %s\n' "$bad" >&2
2384
+ return 1
2385
+ fi
2386
+
2387
+ # 主要 3 group が実際に出現すること (10 preset が 3 group に分布する実データ確認)
2388
+ for label in 'POC' '社内ツール' '本番運用'; do
2389
+ if ! printf '%s' "$body" | grep -q "\"group\"[[:space:]]*:[[:space:]]*\"${label}\""; then
2390
+ printf 'S-50: group "%s" が 1 件も出現しない (3 group 分布の確認失敗、body: %s)\n' "$label" "$body" >&2
2391
+ return 1
2392
+ fi
2393
+ done
2394
+
2395
+ return 0
2396
+ )
2397
+
2398
+ # ============================================================
2399
+ # Case S-51: GET /api/current-preset の match_type が 'preset'|'custom' のいずれか
2400
+ # task-76 Step 2: match_type を 'unsaved' → 'custom' に整理。'unsaved' は廃止語彙。
2401
+ # poc-no-git apply → match_type=preset、known key 変更 → match_type=custom の遷移を 1 case で検証。
2402
+ # ============================================================
2403
+ _case_s51() (
2404
+ set -uo pipefail
2405
+ local port="$1"
2406
+
2407
+ # 1. apply 後は match_type が許容 2 値のいずれか (通常 preset)
2408
+ local apply_code
2409
+ apply_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/preset/poc-no-git/apply" '{}')
2410
+ case "$apply_code" in
2411
+ 200|207) : ;;
2412
+ 4??|5??)
2413
+ printf 'S-51: preset apply returned HTTP %s (権限/yml 破損の疑い、真の FAIL)\n' "$apply_code" >&2
2414
+ return 1
2415
+ ;;
2416
+ *)
2417
+ printf 'S-51: preset apply returned HTTP %s (server 接続不可等の環境 skip)\n' "$apply_code" >&2
2418
+ return 2
2419
+ ;;
2420
+ esac
2421
+
2422
+ local body
2423
+ body=$(_curl_json "http://127.0.0.1:${port}/api/current-preset")
2424
+
2425
+ # match_type は "preset" または "custom" のいずれか (旧 'unsaved' は廃止)
2426
+ if ! printf '%s' "$body" | grep -qE '"match_type"[[:space:]]*:[[:space:]]*"(preset|custom)"'; then
2427
+ printf 'S-51: match_type が "preset"|"custom" でない (旧 unsaved 残存の疑い、body: %s)\n' "$body" >&2
2428
+ return 1
2429
+ fi
2430
+
2431
+ # 'unsaved' という旧語彙が残っていないこと (regression guard)
2432
+ if printf '%s' "$body" | grep -q '"match_type"[[:space:]]*:[[:space:]]*"unsaved"'; then
2433
+ printf 'S-51: match_type に旧語彙 "unsaved" が残存 (task-76 Step 2 で custom へ移行済のはず、body: %s)\n' "$body" >&2
2434
+ return 1
2435
+ fi
2436
+
2437
+ # 2. known key を変更すると match_type=custom になること
2438
+ local set_code
2439
+ set_code=$(_curl_post_json_code "http://127.0.0.1:${port}/api/set" \
2440
+ '{"key":"confidence_threshold","value":"0.99"}')
2441
+ if [ "$set_code" != "200" ]; then
2442
+ printf 'S-51: /api/set confidence_threshold=0.99 returned HTTP %s (known key set 失敗、FAIL)\n' "$set_code" >&2
2443
+ return 1
2444
+ fi
2445
+
2446
+ local after_body
2447
+ after_body=$(_curl_json "http://127.0.0.1:${port}/api/current-preset")
2448
+ if ! printf '%s' "$after_body" | grep -q '"match_type"[[:space:]]*:[[:space:]]*"custom"'; then
2449
+ printf 'S-51: known key 変更後に match_type=custom でない (body: %s)\n' "$after_body" >&2
2450
+ return 1
2451
+ fi
2452
+
2453
+ return 0
2454
+ )
2455
+
2456
+ # ============================================================
2457
+ # task-77 Step 5 新規 case (S-52 / S-53 / S-54): git 統合 policy 実 key 化
2458
+ # §3.5: 10 named preset の values に mainline_integration_policy が入り 3 値のいずれか
2459
+ # §3.6: mainline_branch / mainline_integration_policy が Gate/Confidence category 実 key として右ペインに出る
2460
+ # app.js ENUM_OPTIONS に mainline_integration_policy enum (select 表示)
2461
+ # ============================================================
2462
+
2463
+ # ============================================================
2464
+ # Case S-52: 各 named preset の values に mainline_integration_policy + 3 値分布
2465
+ # /api/presets は values を返さない (axes_values 撤去済、task-63 Step 4 A3) ため、
2466
+ # per-preset の /api/preset/:name/diff (changes[].key/new、後方互換) で values を検証する。
2467
+ # §3.5 の policy 既定: poc-*=local-merge-push / inner-*+production-*=pr-required / harness-development=local-merge
2468
+ # ============================================================
2469
+ _case_s52() (
2470
+ set -uo pipefail
2471
+ local port="$1"
2472
+
2473
+ # 10 named preset 全件
2474
+ local presets="poc-no-git poc-with-git inner-typescript inner-python \
2475
+ production-typescript-personal production-typescript-enterprise production-python \
2476
+ production-rust production-go harness-development"
2477
+
2478
+ # §3.5 期待 policy (preset:policy)
2479
+ local expect_local_merge_push="poc-no-git poc-with-git"
2480
+ local expect_local_merge="harness-development"
2481
+ # 残り (inner-*/production-*) は pr-required
2482
+
2483
+ local saw_pr="" saw_lmp="" saw_lm=""
2484
+ local p
2485
+ for p in $presets; do
2486
+ local body val
2487
+ body=$(_curl_json "http://127.0.0.1:${port}/api/preset/${p}/diff")
2488
+
2489
+ # changes[] から mainline_integration_policy の "new" 値を抽出
2490
+ # 各 change は {"key":"...","current":"...","new":"...",...} の単一 object
2491
+ # mainline_integration_policy を含む change object 部分を切り出し new を取る
2492
+ val=$(printf '%s' "$body" \
2493
+ | grep -oE '"key"[[:space:]]*:[[:space:]]*"mainline_integration_policy"[^}]*"new"[[:space:]]*:[[:space:]]*"[^"]*"' \
2494
+ | grep -oE '"new"[[:space:]]*:[[:space:]]*"[^"]*"' \
2495
+ | sed -E 's/.*"new"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/' || true)
2496
+
2497
+ if [ -z "$val" ]; then
2498
+ printf 'S-52: preset %s の diff に mainline_integration_policy change が無い (values 未追加の疑い、body: %s)\n' "$p" "$body" >&2
2499
+ return 1
2500
+ fi
2501
+
2502
+ # 許容 3 値
2503
+ case "$val" in
2504
+ pr-required|local-merge|local-merge-push) : ;;
2505
+ *)
2506
+ printf 'S-52: preset %s の policy 値 "%s" が許容外 (pr-required/local-merge/local-merge-push)\n' "$p" "$val" >&2
2507
+ return 1
2508
+ ;;
2509
+ esac
2510
+
2511
+ # §3.5 期待値との照合
2512
+ local want="pr-required"
2513
+ case " $expect_local_merge_push " in *" $p "*) want="local-merge-push" ;; esac
2514
+ case " $expect_local_merge " in *" $p "*) want="local-merge" ;; esac
2515
+ if [ "$val" != "$want" ]; then
2516
+ printf 'S-52: preset %s の policy expected %s but got %s (§3.5 違反)\n' "$p" "$want" "$val" >&2
2517
+ return 1
2518
+ fi
2519
+
2520
+ case "$val" in
2521
+ pr-required) saw_pr=1 ;;
2522
+ local-merge-push) saw_lmp=1 ;;
2523
+ local-merge) saw_lm=1 ;;
2524
+ esac
2525
+ done
2526
+
2527
+ # 3 値が実際に分布すること
2528
+ if [ -z "$saw_pr" ] || [ -z "$saw_lmp" ] || [ -z "$saw_lm" ]; then
2529
+ printf 'S-52: 3 policy 値の分布不足 (pr=%s lmp=%s lm=%s)\n' "${saw_pr:-0}" "${saw_lmp:-0}" "${saw_lm:-0}" >&2
2530
+ return 1
2531
+ fi
2532
+
2533
+ return 0
2534
+ )
2535
+
2536
+ # ============================================================
2537
+ # Case S-53: GET /api/keys?category=Gate/Confidence に mainline_branch + mainline_integration_policy が出る
2538
+ # 右ペイン Gate/Confidence accordion に実 key として表示される前提を担保。
2539
+ # ============================================================
2540
+ _case_s53() (
2541
+ set -uo pipefail
2542
+ local port="$1"
2543
+
2544
+ local body
2545
+ # category 名に '/' を含むので URL encode (Gate%2FConfidence)
2546
+ body=$(_curl_json "http://127.0.0.1:${port}/api/keys?category=Gate%2FConfidence")
2547
+
2548
+ local k
2549
+ for k in mainline_branch mainline_integration_policy; do
2550
+ if ! printf '%s' "$body" | grep -q "\"${k}\""; then
2551
+ printf 'S-53: /api/keys?category=Gate/Confidence に "%s" が出ない (Step 1 metadata category 未反映の疑い、body: %s)\n' "$k" "$body" >&2
2552
+ return 1
2553
+ fi
2554
+ done
2555
+
2556
+ return 0
2557
+ )
2558
+
2559
+ # ============================================================
2560
+ # Case S-54: app.js ENUM_OPTIONS に mainline_integration_policy enum (3 値) が静的に存在
2561
+ # file-only (port 不要)。select 表示に必要な client SSoT を grep で確認。
2562
+ # ============================================================
2563
+ _case_s54() (
2564
+ set -uo pipefail
2565
+
2566
+ local app_js="${REPO_ROOT}/.claude/scripts/lib/hc-config-web-ui/app.js"
2567
+ if [ ! -f "$app_js" ]; then
2568
+ printf 'S-54: app.js not found at %s\n' "$app_js" >&2
2569
+ return 2
2570
+ fi
2571
+
2572
+ # ENUM_OPTIONS 内に mainline_integration_policy key が存在
2573
+ if ! grep -q "mainline_integration_policy:" "$app_js"; then
2574
+ printf 'S-54: app.js ENUM_OPTIONS に mainline_integration_policy が無い (select 表示不可)\n' >&2
2575
+ return 1
2576
+ fi
2577
+
2578
+ # 3 値が全て列挙されていること
2579
+ local v
2580
+ for v in 'pr-required' 'local-merge-push' 'local-merge'; do
2581
+ if ! grep -q "'${v}'" "$app_js"; then
2582
+ printf 'S-54: app.js ENUM_OPTIONS に policy 値 "%s" が無い\n' "$v" >&2
2583
+ return 1
2584
+ fi
2585
+ done
2586
+
2587
+ return 0
2588
+ )
2589
+
2590
+ # ============================================================
2591
+ # task-78 Step 1 新規 case (S-55 / S-56): metadata label_ja (5 列目) sidebar 表示
2592
+ # ============================================================
2593
+ # Case S-55: GET /api/keys が各 entry に label_ja field を返す + 主要 key の label 値
2594
+ # server.js parser (loadMetadata) が 5 列目 label_ja を読み、/api/keys enriched entry に
2595
+ # 付与することを検証。主要 key (confidence_threshold / mainline_integration_policy /
2596
+ # feature_draft_flow_guard_enabled) の label が metadata.sh の値と一致することを確認。
2597
+ # ============================================================
2598
+ _case_s55() (
2599
+ set -uo pipefail
2600
+ local port="$1"
2601
+
2602
+ local body
2603
+ body=$(_curl_json "http://127.0.0.1:${port}/api/keys")
2604
+ if ! printf '%s' "$body" | grep -q '"keys"'; then
2605
+ printf 'S-55: /api/keys missing .keys field (body: %s)\n' "$body" >&2
2606
+ return 1
2607
+ fi
2608
+
2609
+ # label_ja field が response に存在すること
2610
+ if ! printf '%s' "$body" | grep -q '"label_ja"'; then
2611
+ printf 'S-55: /api/keys response に label_ja field が無い (body: %s)\n' "$body" >&2
2612
+ return 1
2613
+ fi
2614
+
2615
+ # H-2 (review fix): label_ja 非空率 100% を担保。"label_ja":"" (空文字) が
2616
+ # 1 件でも出現したら FAIL (空白許容、JSON エンコード形式 "label_ja":"" を検出)。
2617
+ if printf '%s' "$body" | grep -qE '"label_ja"[[:space:]]*:[[:space:]]*""'; then
2618
+ printf 'S-55: /api/keys に空の label_ja ("label_ja":"") が存在 (全 key 非空必須)\n' >&2
2619
+ return 1
2620
+ fi
2621
+
2622
+ # 主要 key の label 値が含まれること (metadata.sh の値と一致)
2623
+ local label
2624
+ for label in '信頼度しきい値' '本流統合ポリシー' 'draftフローガード有効化'; do
2625
+ if ! printf '%s' "$body" | grep -q "$label"; then
2626
+ printf 'S-55: /api/keys に主要 label "%s" が無い\n' "$label" >&2
2627
+ return 1
2628
+ fi
2629
+ done
2630
+
2631
+ return 0
2632
+ )
2633
+
2634
+ # ============================================================
2635
+ # Case S-56: metadata table の 5 列化が key parity を壊さない (file-only、port 不要)
2636
+ # 全 metadata 行が 5 列 (key/category/description/effect/label_ja) であること、
2637
+ # 行数が 5 列化前と不変であること、全 key の label_ja が非空であることを検証。
2638
+ # ============================================================
2639
+ _case_s56() (
2640
+ set -uo pipefail
2641
+
2642
+ local meta="${REPO_ROOT}/.claude/scripts/lib/hc-config-metadata.sh"
2643
+ if [ ! -f "$meta" ]; then
2644
+ printf 'S-56: hc-config-metadata.sh not found at %s\n' "$meta" >&2
2645
+ return 1
2646
+ fi
2647
+
2648
+ # source して table を dump、NF / 空 label を bash 内で検査
2649
+ local result
2650
+ result=$(source "$meta" 2>/dev/null && _hc_metadata_table 2>/dev/null | awk -F'\t' '
2651
+ { rows++ }
2652
+ NF != 5 { bad_nf++ }
2653
+ NF == 5 && $5 == "" { empty_label++ }
2654
+ END { printf "%d %d %d", rows, (bad_nf+0), (empty_label+0) }
2655
+ ' || true)
2656
+
2657
+ local rows bad_nf empty_label
2658
+ rows=$(printf '%s' "$result" | awk '{print $1}')
2659
+ bad_nf=$(printf '%s' "$result" | awk '{print $2}')
2660
+ empty_label=$(printf '%s' "$result" | awk '{print $3}')
2661
+
2662
+ # H-1 (review fix): rows>=1 のみだと metadata 行が誤削除されても PASS してしまう。
2663
+ # 5 列化前後で行数不変 (現状 84 行) を担保するため下限 84 を assertion。
2664
+ if [ "${rows:-0}" -lt 84 ]; then
2665
+ printf 'S-56: metadata table 行数が下限 84 未満 (rows=%s、行の誤削除疑い)\n' "$rows" >&2
2666
+ return 1
2667
+ fi
2668
+ if [ "${bad_nf:-1}" != "0" ]; then
2669
+ printf 'S-56: 5 列でない行が %s 件存在 (key parity 破壊)\n' "$bad_nf" >&2
2670
+ return 1
2671
+ fi
2672
+ if [ "${empty_label:-1}" != "0" ]; then
2673
+ printf 'S-56: label_ja が空の key が %s 件存在 (全 key 必須)\n' "$empty_label" >&2
2674
+ return 1
2675
+ fi
2676
+
2677
+ return 0
2678
+ )
2679
+
2680
+ # ============================================================
2681
+ # Case S-57: 変更内容 右サイドバー (task-78 Step 3) の id 契約 + render 関数存在 (file-only、port 不要)
2682
+ # - index.html に #changes-sidebar / #changes-list が id= として実在
2683
+ # - app.js が #changes-list を $()/getElementById で参照 (cross-file 契約乖離 guard)
2684
+ # - app.js に renderChangesSidebar 関数 + keyDisplayLabel (key_name(label) render) が存在
2685
+ # task-76 の id mismatch render 事故 (feedback_parallel_subagent_cross_file_contract_drift) の再発防止。
2686
+ # ============================================================
2687
+ _case_s57() (
2688
+ set -uo pipefail
2689
+ local ui_dir="${REPO_ROOT}/.claude/scripts/lib/hc-config-web-ui"
2690
+ local app_js="${ui_dir}/app.js"
2691
+ local index_html="${ui_dir}/index.html"
2692
+
2693
+ if [ ! -f "$app_js" ] || [ ! -f "$index_html" ]; then
2694
+ printf 'S-57: app.js or index.html not found\n' >&2
2695
+ return 1
2696
+ fi
2697
+
2698
+ # index.html に sidebar container id が実在
2699
+ local id
2700
+ for id in changes-sidebar changes-list; do
2701
+ if ! grep -qE "id=[\"']${id}[\"']" "$index_html" 2>/dev/null; then
2702
+ printf 'S-57: index.html に id="%s" が存在しない (右サイドバー container 欠落)\n' "$id" >&2
2703
+ return 1
2704
+ fi
2705
+ done
2706
+
2707
+ # app.js が changes-list を $()/getElementById で参照 (render target 契約)
2708
+ if ! grep -qE "(\\\$|getElementById)\(['\"]changes-list['\"]\)" "$app_js" 2>/dev/null; then
2709
+ printf 'S-57: app.js が render target id "changes-list" を $()/getElementById で参照していない\n' >&2
2710
+ return 1
2711
+ fi
2712
+
2713
+ # renderChangesSidebar 関数 (差分 render) が定義されている
2714
+ if ! grep -qE "function renderChangesSidebar" "$app_js" 2>/dev/null; then
2715
+ printf 'S-57: app.js に renderChangesSidebar 関数が無い (変更内容 render 不在)\n' >&2
2716
+ return 1
2717
+ fi
2718
+
2719
+ # keyDisplayLabel 関数 (key_name(label_ja) render) が定義されている
2720
+ if ! grep -qE "function keyDisplayLabel" "$app_js" 2>/dev/null; then
2721
+ printf 'S-57: app.js に keyDisplayLabel 関数が無い (key_name(label) render 不在)\n' >&2
2722
+ return 1
2723
+ fi
2724
+
2725
+ return 0
2726
+ )
2727
+
2728
+ # ============================================================
2729
+ # Case S-34: SIGTERM graceful shutdown → port release
2730
+ # iter 4 C: G5 — SIGTERM graceful (S-04 は SIGINT、本 case は SIGTERM)
2731
+ # ============================================================
2732
+ _case_s34() (
2733
+ set -uo pipefail
2734
+
2735
+ if ! _has_node; then
2736
+ printf 'S-34: node not found, skip\n' >&2
2737
+ return 2
2738
+ fi
2739
+
2740
+ local log_file="${TMP_DIR}/s34-server.log"
2741
+ _start_server "$log_file"
2742
+ local pid=$SERVER_PID
2743
+
2744
+ if ! kill -0 "$pid" 2>/dev/null; then
2745
+ printf 'S-34: server did not start\n' >&2
2746
+ return 1
2747
+ fi
2748
+
2749
+ local port
2750
+ port=$(_get_server_port "$log_file")
2751
+ if [ -z "$port" ]; then
2752
+ printf 'S-34: could not detect port\n' >&2
2753
+ kill "$pid" 2>/dev/null || true
2754
+ SERVER_PID=""
2755
+ return 1
2756
+ fi
2757
+
2758
+ # SIGTERM 送信
2759
+ kill -TERM "$pid" 2>/dev/null || true
2760
+ local waited=0
2761
+ while [ $waited -lt 6 ]; do
2762
+ sleep 0.5
2763
+ waited=$((waited + 1))
2764
+ if ! kill -0 "$pid" 2>/dev/null; then
2765
+ break
2766
+ fi
2767
+ done
2768
+ SERVER_PID=""
2769
+
2770
+ if kill -0 "$pid" 2>/dev/null; then
2771
+ printf 'S-34: server still running after SIGTERM\n' >&2
2772
+ kill -9 "$pid" 2>/dev/null || true
2773
+ return 1
2774
+ fi
2775
+
2776
+ # port が解放されているか: 別 server が同 port で起動できるか確認
2777
+ local log2="${TMP_DIR}/s34-check.log"
2778
+ HC_WEB_NO_OPEN=1 node "${WEB_SERVER}" >"$log2" 2>&1 &
2779
+ local pid2=$!
2780
+ sleep 2
2781
+ local port2
2782
+ port2=$(_get_server_port "$log2")
2783
+ kill "$pid2" 2>/dev/null || true
2784
+ wait "$pid2" 2>/dev/null || true
2785
+
2786
+ if [ -z "$port2" ]; then
2787
+ printf 'S-34: port not released after SIGTERM (second server could not start)\n' >&2
2788
+ return 1
2789
+ fi
2790
+
2791
+ return 0
2792
+ )
2793
+
2794
+ # ============================================================
2795
+ # テスト実行
2796
+ # ============================================================
2797
+
2798
+ printf '\n%s\n\n' '=== hc-config-web-ui-smoke (task-63 Step 5: /api/current-preset + top/edit view 5 case) ==='
2799
+
2800
+ # --- server lifecycle (独立 server、各 case で起動/停止) ---
2801
+
2802
+ printf '%s\n' '--- server lifecycle ---'
2803
+
2804
+ if _case_s01 2>/dev/null; then _record PASS "S-01" "server 起動 (HC_WEB_NO_OPEN=1) → port LISTEN → GET / 200/302 → kill"
2805
+ elif [ $? -eq 2 ]; then _record SKIP "S-01" "server 起動 (node not available)"
2806
+ else _record FAIL "S-01" "server 起動 (HC_WEB_NO_OPEN=1) → port LISTEN → GET / 200/302 → kill"
2807
+ fi
2808
+
2809
+ _s02_result=0
2810
+ _case_s02 2>/dev/null || _s02_result=$?
2811
+ if [ $_s02_result -eq 0 ]; then _record PASS "S-02" "port 3060 先 occupy → server → 3061+ で listen"
2812
+ elif [ $_s02_result -eq 2 ]; then _record SKIP "S-02" "port 3060 先 occupy (nc/socat/python3 not available)"
2813
+ else _record FAIL "S-02" "port 3060 先 occupy → server → 3061+ で listen"
2814
+ fi
2815
+
2816
+ _s03_result=0
2817
+ _case_s03 2>/dev/null || _s03_result=$?
2818
+ if [ $_s03_result -eq 0 ]; then _record PASS "S-03" "port 3060-3070 全 occupied → server process.exit(1)"
2819
+ elif [ $_s03_result -eq 2 ]; then _record SKIP "S-03" "port 全 occupied (占有ツール不在)"
2820
+ else _record FAIL "S-03" "port 3060-3070 全 occupied → server process.exit(1)"
2821
+ fi
2822
+
2823
+ _s04_result=0
2824
+ _case_s04 2>/dev/null || _s04_result=$?
2825
+ if [ $_s04_result -eq 0 ]; then _record PASS "S-04" "server 起動 → SIGINT → graceful shutdown → port release"
2826
+ elif [ $_s04_result -eq 2 ]; then _record SKIP "S-04" "SIGINT graceful (node not available)"
2827
+ else _record FAIL "S-04" "server 起動 → SIGINT → graceful shutdown → port release"
2828
+ fi
2829
+
2830
+ # S-34: SIGTERM graceful (独立 server)
2831
+ _s34_result=0
2832
+ _case_s34 2>/dev/null || _s34_result=$?
2833
+ if [ $_s34_result -eq 0 ]; then _record PASS "S-34" "server 起動 → SIGTERM → graceful shutdown → port release"
2834
+ elif [ $_s34_result -eq 2 ]; then _record SKIP "S-34" "SIGTERM graceful (node not available)"
2835
+ else _record FAIL "S-34" "server 起動 → SIGTERM → graceful shutdown → port release"
2836
+ fi
2837
+
2838
+ # --- static + API + preset (共有 server) ---
2839
+ printf '\n%s\n' '--- static / API / preset (shared server) ---'
2840
+
2841
+ # S-01〜S-04,S-34 で使ったポートが解放されるまで待機 (最大 3 秒)
2842
+ _wait_ports_free() (
2843
+ set -uo pipefail
2844
+ local waited=0
2845
+ while [ $waited -lt 6 ]; do
2846
+ local busy=0
2847
+ local p
2848
+ for p in 3060 3061 3062 3063; do
2849
+ if command -v lsof >/dev/null 2>&1; then
2850
+ lsof -i ":$p" >/dev/null 2>/dev/null && busy=1 && break
2851
+ fi
2852
+ done
2853
+ if [ $busy -eq 0 ]; then return 0; fi
2854
+ sleep 0.5
2855
+ waited=$((waited + 1))
2856
+ done
2857
+ return 0
2858
+ )
2859
+ _wait_ports_free
2860
+
2861
+ if _has_node && [ -f "${WEB_SERVER}" ]; then
2862
+ # iter 4 C: T-H2 — ISOLATED_HISTORY_DIR で test isolation
2863
+ # iter 6 B: ISOLATED_PRESETS_DIR で custom-test-*.yml pollution 解消
2864
+ # server が HC_HISTORY_DIR_OVERRIDE / HC_PRESETS_DIR_OVERRIDE を読む実装がある場合は isolated dir が使われる
2865
+ _start_shared_server "$ISOLATED_HISTORY_DIR" "$ISOLATED_PRESETS_DIR"
2866
+
2867
+ if [ -z "$SHARED_PORT" ]; then
2868
+ printf ' WARN: shared server failed to start, skipping shared-server cases\n'
2869
+ for cid in S-05 S-06 S-07 S-08 S-09 S-10 S-11 S-12 S-13 S-14 S-15 S-16 S-19 S-20 S-21 S-23 S-24 S-25 S-27 S-28 S-29 S-30 S-32 S-35 S-36 S-39 S-43 S-44 S-48 S-49 S-50 S-51 S-52 S-53 S-55; do
2870
+ _record SKIP "$cid" "shared server not available"
2871
+ done
2872
+ _record SKIP "S-22" "/api/preset/save 撤去 (task-63 設計簡素化)"
2873
+ _record SKIP "S-26" "/api/preset/save 6 軸欠落 case 撤去 (task-63 設計簡素化)"
2874
+ _record SKIP "S-31" "/api/preset/save path traversal case 撤去 (task-63 設計簡素化)"
2875
+ _record SKIP "S-33" "XSS injection save name case 撤去 (task-63 設計簡素化)"
2876
+ _record SKIP "S-37" "RETIRED: top view (renderTop/banner) removed in task-76 2-pane redesign"
2877
+ _record SKIP "S-38" "RETIRED: edit-view state machine removed in task-76 2-pane redesign"
2878
+ # S-40 は shared server 必須なので SKIP、S-41/S-42 は file-only なので実行
2879
+ _record SKIP "S-40" "POST /api/preset/save 404 (shared server not available)"
2880
+ _s41_result=0
2881
+ _case_s41 2>/dev/null || _s41_result=$?
2882
+ if [ $_s41_result -eq 0 ]; then _record PASS "S-41" "UI 3 file 絵文字 0 件 (絵文字不要 regression guard)"
2883
+ elif [ $_s41_result -eq 2 ]; then _record SKIP "S-41" "絵文字検出 (perl/python3 not available)"
2884
+ else _record FAIL "S-41" "UI 3 file 絵文字 0 件 (絵文字不要 regression guard)"
2885
+ fi
2886
+ if _case_s42 2>/dev/null; then _record PASS "S-42" "app.js DOM id 契約が index.html id= と整合 (DOM id cross-check)"
2887
+ else _record FAIL "S-42" "app.js DOM id 契約が index.html id= と整合 (DOM id cross-check)"
2888
+ fi
2889
+ _s54_result=0
2890
+ _case_s54 2>/dev/null || _s54_result=$?
2891
+ if [ $_s54_result -eq 0 ]; then _record PASS "S-54" "app.js ENUM_OPTIONS に mainline_integration_policy enum (3 値、select 表示)"
2892
+ elif [ $_s54_result -eq 2 ]; then _record SKIP "S-54" "app.js not found"
2893
+ else _record FAIL "S-54" "app.js ENUM_OPTIONS に mainline_integration_policy enum (3 値、select 表示)"
2894
+ fi
2895
+ # S-43/S-44 は shared server 必須なので SKIP、S-45/S-46 は file-only なので実行
2896
+ _record SKIP "S-43" "preset axes 6 key (shared server not available)"
2897
+ _record SKIP "S-44" "unsaved axes:null (shared server not available)"
2898
+ _record SKIP "S-45" "RETIRED: top-view axes table (cp.axes/AXIS_LABELS_JA) removed in task-76 redesign"
2899
+ _record SKIP "S-46" "RETIRED: edit view / applyPresetMode removed in task-76 2-pane redesign"
2900
+ # S-56 は file-only (port 不要) なので shared server 不在でも実行 (task-78 Step 1)
2901
+ if _case_s56 2>/dev/null; then _record PASS "S-56" "metadata table 5 列化 (全 key label_ja 非空、NF==5、key parity 不変)"
2902
+ else _record FAIL "S-56" "metadata table 5 列化 (全 key label_ja 非空、NF==5、key parity 不変)"
2903
+ fi
2904
+ else
2905
+ _PORT="$SHARED_PORT"
2906
+
2907
+ if _case_s05 "$_PORT" 2>/dev/null; then _record PASS "S-05" "GET / → 302 or 200"
2908
+ else _record FAIL "S-05" "GET / → 302 or 200"
2909
+ fi
2910
+
2911
+ if _case_s06 "$_PORT" 2>/dev/null; then _record PASS "S-06" "GET /static/index.html → 200 + text/html"
2912
+ else _record FAIL "S-06" "GET /static/index.html → 200 + text/html"
2913
+ fi
2914
+
2915
+ if _case_s07 "$_PORT" 2>/dev/null; then _record PASS "S-07" "GET /static/app.js → 200 + application/javascript"
2916
+ else _record FAIL "S-07" "GET /static/app.js → 200 + application/javascript"
2917
+ fi
2918
+
2919
+ if _case_s08 "$_PORT" 2>/dev/null; then _record PASS "S-08" "path traversal /static/../../ → 403/404"
2920
+ else _record FAIL "S-08" "path traversal /static/../../ → 403/404"
2921
+ fi
2922
+
2923
+ if _case_s09 "$_PORT" 2>/dev/null; then _record PASS "S-09" "GET /api/categories → 200 + .categories length >= 1"
2924
+ else _record FAIL "S-09" "GET /api/categories → 200 + .categories length >= 1"
2925
+ fi
2926
+
2927
+ if _case_s10 "$_PORT" 2>/dev/null; then _record PASS "S-10" "GET /api/keys → 200 + .keys length >= 1"
2928
+ else _record FAIL "S-10" "GET /api/keys → 200 + .keys length >= 1"
2929
+ fi
2930
+
2931
+ if _case_s11 "$_PORT" 2>/dev/null; then _record PASS "S-11" "GET /api/presets → 200 + .presets length == 10"
2932
+ else _record FAIL "S-11" "GET /api/presets → 200 + .presets length == 10"
2933
+ fi
2934
+
2935
+ if _case_s12 "$_PORT" 2>/dev/null; then _record PASS "S-12" "GET /api/preset/poc-no-git/diff → 200 + .changes[].key/current/new/effect"
2936
+ else _record FAIL "S-12" "GET /api/preset/poc-no-git/diff → 200 + .changes[].key/current/new/effect"
2937
+ fi
2938
+
2939
+ if _case_s13 "$_PORT" 2>/dev/null; then _record PASS "S-13" "POST /api/set 不正 key (空/欠落) → 400"
2940
+ else _record FAIL "S-13" "POST /api/set 不正 key (空/欠落) → 400"
2941
+ fi
2942
+
2943
+ if _case_s14 "$_PORT" "$ISOLATED_HISTORY_DIR" 2>/dev/null; then _record PASS "S-14" "POST /api/preset/poc-no-git/apply → 200/207 + history file 生成"
2944
+ else _record FAIL "S-14" "POST /api/preset/poc-no-git/apply → 200/207 + history file 生成"
2945
+ fi
2946
+
2947
+ if _case_s15 "$_PORT" 2>/dev/null; then _record PASS "S-15" "apply → rollback → 200 + ok:true"
2948
+ else _record FAIL "S-15" "apply → rollback → 200 + ok:true"
2949
+ fi
2950
+
2951
+ if _case_s16 "$_PORT" 2>/dev/null; then _record PASS "S-16" "rollback timestamp traversal (plain + URL encoded) → 400/404/500"
2952
+ else _record FAIL "S-16" "rollback timestamp traversal (plain + URL encoded) → 400/404/500"
2953
+ fi
2954
+
2955
+ _s19_result=0
2956
+ _case_s19 "$_PORT" 2>/dev/null || _s19_result=$?
2957
+ if [ $_s19_result -eq 0 ]; then _record PASS "S-19" "1MB+1byte body POST /api/set → 400/413"
2958
+ elif [ $_s19_result -eq 2 ]; then _record SKIP "S-19" "large body (python3/dd not available)"
2959
+ else _record FAIL "S-19" "1MB+1byte body POST /api/set → 400/413"
2960
+ fi
2961
+
2962
+ # --- iter 4 C 新規 case ---
2963
+ printf '\n%s\n' '--- iter 4 C 新規 case (S-20〜S-33) ---'
2964
+
2965
+ _s20_result=0
2966
+ _case_s20 "$_PORT" 2>/dev/null || _s20_result=$?
2967
+ if [ $_s20_result -eq 0 ]; then _record PASS "S-20" "abort rollback silent no-op verify"
2968
+ elif [ $_s20_result -eq 2 ]; then _record SKIP "S-20" "abort rollback (no history)"
2969
+ else _record FAIL "S-20" "abort rollback silent no-op verify"
2970
+ fi
2971
+
2972
+ if _case_s21 "$_PORT" 2>/dev/null; then _record PASS "S-21" "unknown preset → 404 (diff + apply)"
2973
+ else _record FAIL "S-21" "unknown preset → 404 (diff + apply)"
2974
+ fi
2975
+
2976
+ _record SKIP "S-22" "/api/preset/save 撤去 (task-63 設計簡素化)"
2977
+
2978
+ if _case_s23 "$_PORT" 2>/dev/null; then _record PASS "S-23" "apply response ok + applied + partial フィールド verify"
2979
+ else _record FAIL "S-23" "apply response ok + applied + partial フィールド verify"
2980
+ fi
2981
+
2982
+ if _case_s24 "$_PORT" "$ISOLATED_HISTORY_DIR" 2>/dev/null; then _record PASS "S-24" "HISTORY_DIR 不在 → apply で自動作成"
2983
+ else _record FAIL "S-24" "HISTORY_DIR 不在 → apply で自動作成"
2984
+ fi
2985
+
2986
+ if _case_s25 "$_PORT" 2>/dev/null; then _record PASS "S-25" "invalid JSON body → 400 (/api/set のみ)"
2987
+ else _record FAIL "S-25" "invalid JSON body → 400 (/api/set のみ)"
2988
+ fi
2989
+
2990
+ _record SKIP "S-26" "/api/preset/save 6 軸欠落 case 撤去 (task-63 設計簡素化)"
2991
+
2992
+ if _case_s27 "$_PORT" 2>/dev/null; then _record PASS "S-27" "POST /api/set empty string value → 200 or 400 (仕様確認)"
2993
+ else _record FAIL "S-27" "POST /api/set empty string value → 200 or 400 (仕様確認)"
2994
+ fi
2995
+
2996
+ if _case_s28 "$_PORT" 2>/dev/null; then _record PASS "S-28" "URL encoded rollback traversal → 400/404/500 (not 200)"
2997
+ else _record FAIL "S-28" "URL encoded rollback traversal → 400/404/500 (not 200)"
2998
+ fi
2999
+
3000
+ if _case_s29 "$_PORT" 2>/dev/null; then _record PASS "S-29" "GET /api/preset/history → .history array"
3001
+ else _record FAIL "S-29" "GET /api/preset/history → .history array"
3002
+ fi
3003
+
3004
+ _s30_result=0
3005
+ _case_s30 "$_PORT" 2>/dev/null || _s30_result=$?
3006
+ if [ $_s30_result -eq 0 ]; then _record PASS "S-30" "rollback → ok + restored フィールド verify"
3007
+ elif [ $_s30_result -eq 2 ]; then _record SKIP "S-30" "rollback 0 件 (no history)"
3008
+ else _record FAIL "S-30" "rollback → ok + restored フィールド verify"
3009
+ fi
3010
+
3011
+ _record SKIP "S-31" "/api/preset/save path traversal case 撤去 (task-63 設計簡素化)"
3012
+
3013
+ _s32_result=0
3014
+ _case_s32 "$_PORT" 2>/dev/null || _s32_result=$?
3015
+ if [ $_s32_result -eq 0 ]; then _record PASS "S-32" "GET /api/keys?category=<name> → filtered list"
3016
+ elif [ $_s32_result -eq 2 ]; then _record SKIP "S-32" "category filter (no categories found)"
3017
+ else _record FAIL "S-32" "GET /api/keys?category=<name> → filtered list"
3018
+ fi
3019
+
3020
+ _record SKIP "S-33" "XSS injection save name case 撤去 (task-63 設計簡素化)"
3021
+
3022
+ # --- task-63 Step 5 新規 case ---
3023
+ printf '\n%s\n' '--- task-63 Step 5 新規 case (S-35〜S-39) ---'
3024
+
3025
+ if _case_s35 "$_PORT" 2>/dev/null; then _record PASS "S-35" "GET /api/current-preset → match_type + display_name_ja field"
3026
+ else _record FAIL "S-35" "GET /api/current-preset → match_type + display_name_ja field"
3027
+ fi
3028
+
3029
+ _s36_result=0
3030
+ _case_s36 "$_PORT" 2>/dev/null || _s36_result=$?
3031
+ if [ $_s36_result -eq 0 ]; then _record PASS "S-36" "preset apply 後 /api/current-preset → match_type=preset"
3032
+ elif [ $_s36_result -eq 2 ]; then _record SKIP "S-36" "preset apply skip (apply failed)"
3033
+ else _record FAIL "S-36" "preset apply 後 /api/current-preset → match_type=preset"
3034
+ fi
3035
+
3036
+ _record SKIP "S-37" "RETIRED: top view (renderTop/banner) removed in task-76 2-pane redesign"
3037
+ _record SKIP "S-38" "RETIRED: edit-view state machine removed in task-76 2-pane redesign"
3038
+
3039
+ _s39_result=0
3040
+ _case_s39 "$_PORT" 2>/dev/null || _s39_result=$?
3041
+ if [ $_s39_result -eq 0 ]; then _record PASS "S-39" "/api/set 1 key 変更 → /api/current-preset match_type=unsaved"
3042
+ elif [ $_s39_result -eq 2 ]; then _record SKIP "S-39" "/api/set unsaved 確認 skip (server 接続不可)"
3043
+ else _record FAIL "S-39" "/api/set 1 key 変更 → /api/current-preset match_type=unsaved"
3044
+ fi
3045
+
3046
+ # --- task-63 Step 6 iter-2 新規 negative case (S-40 / S-41) ---
3047
+ printf '\n%s\n' '--- task-63 Step 6 iter-2 negative case (S-40 / S-41) ---'
3048
+
3049
+ # F4: /api/preset/save 撤去 regression guard (404 負テスト)
3050
+ if _case_s40 "$_PORT" 2>/dev/null; then _record PASS "S-40" "POST /api/preset/save → 404 (custom 保存撤去 regression guard)"
3051
+ else _record FAIL "S-40" "POST /api/preset/save → 404 (custom 保存撤去 regression guard)"
3052
+ fi
3053
+
3054
+ # F5: UI 3 file 絵文字 0 件 regression guard (file-only、port 不要)
3055
+ _s41_result=0
3056
+ _case_s41 2>/dev/null || _s41_result=$?
3057
+ if [ $_s41_result -eq 0 ]; then _record PASS "S-41" "UI 3 file 絵文字 0 件 (絵文字不要 regression guard)"
3058
+ elif [ $_s41_result -eq 2 ]; then _record SKIP "S-41" "絵文字検出 (perl/python3 not available)"
3059
+ else _record FAIL "S-41" "UI 3 file 絵文字 0 件 (絵文字不要 regression guard)"
3060
+ fi
3061
+
3062
+ # task-63 Step 7: DOM id 契約 cross-check (file-only、port 不要)
3063
+ if _case_s42 2>/dev/null; then _record PASS "S-42" "app.js DOM id 契約が index.html id= と整合 (DOM id cross-check)"
3064
+ else _record FAIL "S-42" "app.js DOM id 契約が index.html id= と整合 (DOM id cross-check)"
3065
+ fi
3066
+ _s54_result=0
3067
+ _case_s54 2>/dev/null || _s54_result=$?
3068
+ if [ $_s54_result -eq 0 ]; then _record PASS "S-54" "app.js ENUM_OPTIONS に mainline_integration_policy enum (3 値、select 表示)"
3069
+ elif [ $_s54_result -eq 2 ]; then _record SKIP "S-54" "app.js not found"
3070
+ else _record FAIL "S-54" "app.js ENUM_OPTIONS に mainline_integration_policy enum (3 値、select 表示)"
3071
+ fi
3072
+
3073
+ # --- task-65 新規 case (S-43〜S-46): axes 返却 + top 6 軸 + dropdown 撤去 ---
3074
+ printf '\n%s\n' '--- task-65 新規 case (S-43〜S-46) ---'
3075
+
3076
+ _s43_result=0
3077
+ _case_s43 "$_PORT" 2>/dev/null || _s43_result=$?
3078
+ if [ $_s43_result -eq 0 ]; then _record PASS "S-43" "preset apply 後 /api/current-preset → axes object (6 key)"
3079
+ elif [ $_s43_result -eq 2 ]; then _record SKIP "S-43" "axes 6 key 確認 skip (apply failed)"
3080
+ else _record FAIL "S-43" "preset apply 後 /api/current-preset → axes object (6 key)"
3081
+ fi
3082
+
3083
+ _s44_result=0
3084
+ _case_s44 "$_PORT" 2>/dev/null || _s44_result=$?
3085
+ if [ $_s44_result -eq 0 ]; then _record PASS "S-44" "unsaved 状態で /api/current-preset → axes:null"
3086
+ elif [ $_s44_result -eq 2 ]; then _record SKIP "S-44" "axes:null 確認 skip (apply failed)"
3087
+ else _record FAIL "S-44" "unsaved 状態で /api/current-preset → axes:null"
3088
+ fi
3089
+
3090
+ # S-45 / S-46 は task-76 で RETIRED (top view / edit view 廃止)
3091
+ _record SKIP "S-45" "RETIRED: top-view axes table (cp.axes/AXIS_LABELS_JA) removed in task-76 redesign"
3092
+ _record SKIP "S-46" "RETIRED: edit view / applyPresetMode removed in task-76 2-pane redesign"
3093
+
3094
+ # --- task-69 Step 3 新規 case (key parity) ---
3095
+ _s47_result=0
3096
+ _case_s47 "$_PORT" 2>/dev/null || _s47_result=$?
3097
+ if [ $_s47_result -eq 0 ]; then _record PASS "S-47" "GET /api/keys key set == yml top-level keys (full parity)"
3098
+ elif [ $_s47_result -eq 2 ]; then _record SKIP "S-47" "key parity (server 接続不可)"
3099
+ else _record FAIL "S-47" "GET /api/keys key set == yml top-level keys (full parity)"
3100
+ fi
3101
+
3102
+ # --- task-76 Step 1 新規 case (S-48 / S-49): diff Failed to fetch 修復 ---
3103
+ printf '\n%s\n' '--- task-76 Step 1 新規 case (S-48 / S-49) ---'
3104
+
3105
+ if _case_s48 "$_PORT" 2>/dev/null; then _record PASS "S-48" "GET /api/preset/:name/diff 正常 JSON + changed field 後方互換"
3106
+ else _record FAIL "S-48" "GET /api/preset/:name/diff 正常 JSON + changed field 後方互換"
3107
+ fi
3108
+
3109
+ if _case_s49 "$_PORT" 2>/dev/null; then _record PASS "S-49" "diff 連続/並列呼び出しの応答性 (Failed to fetch 回帰防止)"
3110
+ else _record FAIL "S-49" "diff 連続/並列呼び出しの応答性 (Failed to fetch 回帰防止)"
3111
+ fi
3112
+
3113
+ # --- task-76 Step 2 新規 case (S-50 / S-51): preset group + match_type custom ---
3114
+ printf '\n%s\n' '--- task-76 Step 2 新規 case (S-50 / S-51) ---'
3115
+
3116
+ if _case_s50 "$_PORT" 2>/dev/null; then _record PASS "S-50" "GET /api/presets 各 entry に group (POC/社内ツール/本番運用/その他) + 3 group 分布"
3117
+ else _record FAIL "S-50" "GET /api/presets 各 entry に group (POC/社内ツール/本番運用/その他) + 3 group 分布"
3118
+ fi
3119
+
3120
+ _s51_result=0
3121
+ _case_s51 "$_PORT" 2>/dev/null || _s51_result=$?
3122
+ if [ $_s51_result -eq 0 ]; then _record PASS "S-51" "GET /api/current-preset match_type が preset|custom (unsaved 廃止)"
3123
+ elif [ $_s51_result -eq 2 ]; then _record SKIP "S-51" "match_type custom 確認 skip (apply failed)"
3124
+ else _record FAIL "S-51" "GET /api/current-preset match_type が preset|custom (unsaved 廃止)"
3125
+ fi
3126
+
3127
+ # --- task-77 Step 5 新規 case (S-52 / S-53): git 統合 policy 実 key 化 ---
3128
+ printf '\n%s\n' '--- task-77 Step 5 新規 case (S-52 / S-53) ---'
3129
+
3130
+ if _case_s52 "$_PORT" 2>/dev/null; then _record PASS "S-52" "各 named preset の diff に mainline_integration_policy (§3.5 既定値 + 3 値分布)"
3131
+ else _record FAIL "S-52" "各 named preset の diff に mainline_integration_policy (§3.5 既定値 + 3 値分布)"
3132
+ fi
3133
+
3134
+ if _case_s53 "$_PORT" 2>/dev/null; then _record PASS "S-53" "GET /api/keys?category=Gate/Confidence に mainline_branch + mainline_integration_policy"
3135
+ else _record FAIL "S-53" "GET /api/keys?category=Gate/Confidence に mainline_branch + mainline_integration_policy"
3136
+ fi
3137
+
3138
+ # --- task-78 Step 1 新規 case (S-55 / S-56): metadata label_ja sidebar 表示 ---
3139
+ printf '\n%s\n' '--- task-78 Step 1 新規 case (S-55 / S-56) ---'
3140
+
3141
+ if _case_s55 "$_PORT" 2>/dev/null; then _record PASS "S-55" "GET /api/keys が label_ja field + 主要 label 値を返す"
3142
+ else _record FAIL "S-55" "GET /api/keys が label_ja field + 主要 label 値を返す"
3143
+ fi
3144
+
3145
+ if _case_s56 2>/dev/null; then _record PASS "S-56" "metadata table 5 列化 (全 key label_ja 非空、NF==5、key parity 不変)"
3146
+ else _record FAIL "S-56" "metadata table 5 列化 (全 key label_ja 非空、NF==5、key parity 不変)"
3147
+ fi
3148
+
3149
+ # --- task-78 Step 3 新規 case (S-57): 変更内容 右サイドバー id 契約 + render 関数 ---
3150
+ if _case_s57 2>/dev/null; then _record PASS "S-57" "変更内容 右サイドバー id 契約 (changes-sidebar/changes-list) + renderChangesSidebar/keyDisplayLabel 存在"
3151
+ else _record FAIL "S-57" "変更内容 右サイドバー id 契約 (changes-sidebar/changes-list) + renderChangesSidebar/keyDisplayLabel 存在"
3152
+ fi
3153
+
3154
+ _stop_shared_server
3155
+ fi
3156
+ else
3157
+ for cid in S-05 S-06 S-07 S-08 S-09 S-10 S-11 S-12 S-13 S-14 S-15 S-16 S-19 S-20 S-21 S-23 S-24 S-25 S-27 S-28 S-29 S-30 S-32 S-35 S-36 S-39 S-40 S-43 S-44 S-47 S-48 S-49 S-50 S-51 S-52 S-53 S-55; do
3158
+ _record SKIP "$cid" "node or hc-config-web-server.js not available"
3159
+ done
3160
+ # S-41 / S-42 は file-only (port 不要) なので node 不在でも実行。
3161
+ # S-37 / S-38 / S-45 / S-46 は task-76 で RETIRED (top/edit view 廃止)。
3162
+ _record SKIP "S-37" "RETIRED: top view (renderTop/banner) removed in task-76 2-pane redesign"
3163
+ _record SKIP "S-38" "RETIRED: edit-view state machine removed in task-76 2-pane redesign"
3164
+ _s41_result=0
3165
+ _case_s41 2>/dev/null || _s41_result=$?
3166
+ if [ $_s41_result -eq 0 ]; then _record PASS "S-41" "UI 3 file 絵文字 0 件 (絵文字不要 regression guard)"
3167
+ elif [ $_s41_result -eq 2 ]; then _record SKIP "S-41" "絵文字検出 (perl/python3 not available)"
3168
+ else _record FAIL "S-41" "UI 3 file 絵文字 0 件 (絵文字不要 regression guard)"
3169
+ fi
3170
+ if _case_s42 2>/dev/null; then _record PASS "S-42" "app.js DOM id 契約が index.html id= と整合 (DOM id cross-check)"
3171
+ else _record FAIL "S-42" "app.js DOM id 契約が index.html id= と整合 (DOM id cross-check)"
3172
+ fi
3173
+ _s54_result=0
3174
+ _case_s54 2>/dev/null || _s54_result=$?
3175
+ if [ $_s54_result -eq 0 ]; then _record PASS "S-54" "app.js ENUM_OPTIONS に mainline_integration_policy enum (3 値、select 表示)"
3176
+ elif [ $_s54_result -eq 2 ]; then _record SKIP "S-54" "app.js not found"
3177
+ else _record FAIL "S-54" "app.js ENUM_OPTIONS に mainline_integration_policy enum (3 値、select 表示)"
3178
+ fi
3179
+ _record SKIP "S-45" "RETIRED: top-view axes table (cp.axes/AXIS_LABELS_JA) removed in task-76 redesign"
3180
+ _record SKIP "S-46" "RETIRED: edit view / applyPresetMode removed in task-76 2-pane redesign"
3181
+ # S-56 / S-57 は file-only (port 不要) なので node 不在でも実行 (task-78 Step 1/3)
3182
+ if _case_s56 2>/dev/null; then _record PASS "S-56" "metadata table 5 列化 (全 key label_ja 非空、NF==5、key parity 不変)"
3183
+ else _record FAIL "S-56" "metadata table 5 列化 (全 key label_ja 非空、NF==5、key parity 不変)"
3184
+ fi
3185
+ if _case_s57 2>/dev/null; then _record PASS "S-57" "変更内容 右サイドバー id 契約 (changes-sidebar/changes-list) + renderChangesSidebar/keyDisplayLabel 存在"
3186
+ else _record FAIL "S-57" "変更内容 右サイドバー id 契約 (changes-sidebar/changes-list) + renderChangesSidebar/keyDisplayLabel 存在"
3187
+ fi
3188
+ fi
3189
+
3190
+ # --- legacy fallback + edge ---
3191
+ printf '\n%s\n' '--- legacy fallback + edge ---'
3192
+
3193
+ if _case_s17 2>/dev/null; then _record PASS "S-17" "HC_HC_CONFIG_TUI_LEGACY=true → TUI 経路 dispatcher 確認"
3194
+ else _record FAIL "S-17" "HC_HC_CONFIG_TUI_LEGACY=true → TUI 経路 dispatcher 確認"
3195
+ fi
3196
+
3197
+ _s18_result=0
3198
+ _case_s18 2>/dev/null || _s18_result=$?
3199
+ if [ $_s18_result -eq 0 ]; then _record PASS "S-18" "node 不在 → WARN stderr + TUI fallback"
3200
+ elif [ $_s18_result -eq 2 ]; then _record SKIP "S-18" "node 不在 (node が /usr/bin 以外にも存在しない)"
3201
+ else _record FAIL "S-18" "node 不在 → WARN stderr + TUI fallback"
3202
+ fi
3203
+
3204
+ # --- 手動 case コメント (Step 6 で実施) ---
3205
+ printf '\n%s\n' '--- manual cases (Step 6 で実施、以下はコメントのみ) ---'
3206
+ printf ' SKIP M-01: browser で preset 選択 → diff preview → checkbox toggle → Apply → history 追加 (目視)\n'
3207
+ printf ' SKIP M-02: category 選択 → key 一覧 → 編集 → Apply → 値反映確認 (目視)\n'
3208
+ printf ' SKIP M-03: Rollback ボタン → confirm dialog → 確認 → 元値復元 (目視)\n'
3209
+ printf ' SKIP M-04: Tailwind CDN offline で degradation 動作確認 (warning banner + legacy 案内)\n'
3210
+ SKIP=$((SKIP + 4))
3211
+
3212
+ # ============================================================
3213
+ # 集計
3214
+ # ============================================================
3215
+
3216
+ TOTAL=$((PASS + FAIL + SKIP))
3217
+ printf '\n%s %d/%d PASS, %d SKIP, %d FAIL ---\n' '--- Result:' "$PASS" "$TOTAL" "$SKIP" "$FAIL"
3218
+
3219
+ if [ "$FAIL" -gt 0 ]; then
3220
+ printf 'FAILED cases:%s\n' "$FAILED_CASES"
3221
+ exit 1
3222
+ fi
3223
+
3224
+ exit 0