agentic-dev 0.2.11 → 0.2.13

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 (276) hide show
  1. package/README.md +72 -54
  2. package/bin/agentic-dev.mjs +162 -11
  3. package/lib/github.mjs +246 -0
  4. package/lib/orchestration-assets.mjs +249 -0
  5. package/lib/scaffold.mjs +89 -0
  6. package/package.json +8 -19
  7. package/.dockerignore +0 -8
  8. package/.env.example +0 -50
  9. package/.gitignore +0 -16
  10. package/AGENTS.md +0 -86
  11. package/SDD_SKILL.md +0 -589
  12. package/compose.yml +0 -206
  13. package/infra/compose/.env.dev.example +0 -28
  14. package/infra/compose/.env.prod.example +0 -29
  15. package/infra/compose/README.md +0 -35
  16. package/infra/compose/dev.yml +0 -125
  17. package/infra/compose/prod.yml +0 -126
  18. package/infra/terraform/README.md +0 -34
  19. package/infra/terraform/aws/data/.terraform.lock.hcl +0 -25
  20. package/infra/terraform/aws/data/README.md +0 -18
  21. package/infra/terraform/aws/data/main.tf +0 -147
  22. package/infra/terraform/aws/data/outputs.tf +0 -14
  23. package/infra/terraform/aws/data/variables.tf +0 -57
  24. package/infra/terraform/aws/data/versions.tf +0 -10
  25. package/infra/terraform/aws/domain/.terraform.lock.hcl +0 -25
  26. package/infra/terraform/aws/domain/README.md +0 -20
  27. package/infra/terraform/aws/domain/env/dev.tfvars.example +0 -6
  28. package/infra/terraform/aws/domain/env/prod.tfvars.example +0 -7
  29. package/infra/terraform/aws/domain/main.tf +0 -149
  30. package/infra/terraform/aws/domain/outputs.tf +0 -29
  31. package/infra/terraform/aws/domain/variables.tf +0 -58
  32. package/infra/terraform/aws/domain/versions.tf +0 -10
  33. package/infra/terraform/openstack/README.md +0 -38
  34. package/infra/terraform/openstack/dev/.terraform.lock.hcl +0 -24
  35. package/infra/terraform/openstack/dev/README.md +0 -18
  36. package/infra/terraform/openstack/dev/main.tf +0 -49
  37. package/infra/terraform/openstack/dev/providers.tf +0 -15
  38. package/infra/terraform/openstack/dev/terraform.tfvars.example +0 -54
  39. package/infra/terraform/openstack/dev/variables.tf +0 -210
  40. package/infra/terraform/openstack/dev/versions.tf +0 -10
  41. package/infra/terraform/openstack/modules/environment_host/main.tf +0 -143
  42. package/infra/terraform/openstack/modules/environment_host/outputs.tf +0 -25
  43. package/infra/terraform/openstack/modules/environment_host/templates/docker-host-user-data.sh.tftpl +0 -40
  44. package/infra/terraform/openstack/modules/environment_host/variables.tf +0 -145
  45. package/infra/terraform/openstack/modules/environment_host/versions.tf +0 -7
  46. package/infra/terraform/openstack/prod/.terraform.lock.hcl +0 -24
  47. package/infra/terraform/openstack/prod/README.md +0 -18
  48. package/infra/terraform/openstack/prod/main.tf +0 -49
  49. package/infra/terraform/openstack/prod/providers.tf +0 -15
  50. package/infra/terraform/openstack/prod/terraform.tfvars.example +0 -55
  51. package/infra/terraform/openstack/prod/variables.tf +0 -210
  52. package/infra/terraform/openstack/prod/versions.tf +0 -10
  53. package/infra/terraform/openstack/server/.terraform.lock.hcl +0 -45
  54. package/infra/terraform/openstack/server/README.md +0 -47
  55. package/infra/terraform/openstack/server/main.tf +0 -161
  56. package/infra/terraform/openstack/server/outputs.tf +0 -30
  57. package/infra/terraform/openstack/server/providers.tf +0 -30
  58. package/infra/terraform/openstack/server/templates/server-user-data.sh.tftpl +0 -50
  59. package/infra/terraform/openstack/server/variables.tf +0 -233
  60. package/infra/terraform/openstack/server/zz_aspace.auto.tfvars.example.json +0 -29
  61. package/pnpm-workspace.yaml +0 -2
  62. package/scripts/dev/audit_sdd_build_ast.py +0 -277
  63. package/sdd/01_planning/01_feature/INDEX.md +0 -16
  64. package/sdd/01_planning/01_feature/README.md +0 -76
  65. package/sdd/01_planning/01_feature/alerts_feature_spec.md +0 -55
  66. package/sdd/01_planning/01_feature/auth_feature_spec.md +0 -57
  67. package/sdd/01_planning/01_feature/catalog_feature_spec.md +0 -61
  68. package/sdd/01_planning/01_feature/fulfillment_feature_spec.md +0 -58
  69. package/sdd/01_planning/01_feature/health_feature_spec.md +0 -52
  70. package/sdd/01_planning/01_feature/inventory_feature_spec.md +0 -60
  71. package/sdd/01_planning/01_feature/order_feature_spec.md +0 -63
  72. package/sdd/01_planning/01_feature/shipping_feature_spec.md +0 -55
  73. package/sdd/01_planning/01_feature/support_feature_spec.md +0 -53
  74. package/sdd/01_planning/01_feature/user_feature_spec.md +0 -54
  75. package/sdd/01_planning/02_screen/INDEX.md +0 -13
  76. package/sdd/01_planning/02_screen/README.md +0 -41
  77. package/sdd/01_planning/02_screen/admin_screen_spec.pdf +0 -0
  78. package/sdd/01_planning/02_screen/assets/README.md +0 -16
  79. package/sdd/01_planning/02_screen/assets/example/README.md +0 -13
  80. package/sdd/01_planning/02_screen/landing_screen_spec.pdf +0 -0
  81. package/sdd/01_planning/02_screen/mobile_screen_spec.pdf +0 -0
  82. package/sdd/01_planning/02_screen/web_screen_spec.pdf +0 -0
  83. package/sdd/01_planning/03_architecture/INDEX.md +0 -9
  84. package/sdd/01_planning/03_architecture/README.md +0 -25
  85. package/sdd/01_planning/03_architecture/architecture_document_structure.md +0 -77
  86. package/sdd/01_planning/03_architecture/backend/README.md +0 -10
  87. package/sdd/01_planning/03_architecture/frontend/README.md +0 -12
  88. package/sdd/01_planning/03_architecture/infra/README.md +0 -10
  89. package/sdd/01_planning/03_architecture/tech-research/README.md +0 -4
  90. package/sdd/01_planning/03_architecture/templates_system_architecture.md +0 -84
  91. package/sdd/01_planning/04_data/INDEX.md +0 -4
  92. package/sdd/01_planning/04_data/README.md +0 -10
  93. package/sdd/01_planning/04_data/templates_data_modeling.md +0 -119
  94. package/sdd/01_planning/05_api/README.md +0 -12
  95. package/sdd/01_planning/05_api/templates_api_contract.md +0 -90
  96. package/sdd/01_planning/06_iac/README.md +0 -11
  97. package/sdd/01_planning/06_iac/templates_runtime_and_cicd_baseline.md +0 -46
  98. package/sdd/01_planning/07_integration/README.md +0 -11
  99. package/sdd/01_planning/07_integration/templates_frontend_api_integration.md +0 -46
  100. package/sdd/01_planning/08_nonfunctional/README.md +0 -7
  101. package/sdd/01_planning/09_security/README.md +0 -7
  102. package/sdd/01_planning/10_test/README.md +0 -12
  103. package/sdd/01_planning/10_test/templates_test_strategy.md +0 -60
  104. package/sdd/01_planning/INDEX.md +0 -19
  105. package/sdd/01_planning/README.md +0 -17
  106. package/sdd/02_plan/01_feature/README.md +0 -34
  107. package/sdd/02_plan/01_feature/_feature_todo_template.md +0 -29
  108. package/sdd/02_plan/02_screen/INDEX.md +0 -19
  109. package/sdd/02_plan/02_screen/README.md +0 -39
  110. package/sdd/02_plan/02_screen/_screen_todo_template.md +0 -60
  111. package/sdd/02_plan/03_architecture/README.md +0 -23
  112. package/sdd/02_plan/03_architecture/architecture_document_governance.md +0 -40
  113. package/sdd/02_plan/03_architecture/build_ast_runtime_tree_governance.md +0 -53
  114. package/sdd/02_plan/03_architecture/repository_governance.md +0 -39
  115. package/sdd/02_plan/03_architecture/runtime_and_structure_governance.md +0 -38
  116. package/sdd/02_plan/03_architecture/templates-hexagonal-template-architecture.md +0 -9
  117. package/sdd/02_plan/03_architecture/toolchain_governance.md +0 -98
  118. package/sdd/02_plan/04_data/README.md +0 -5
  119. package/sdd/02_plan/05_api/README.md +0 -5
  120. package/sdd/02_plan/06_iac/README.md +0 -11
  121. package/sdd/02_plan/06_iac/dev_runtime_delivery.md +0 -36
  122. package/sdd/02_plan/06_iac/template_runtime_delivery.md +0 -50
  123. package/sdd/02_plan/07_integration/README.md +0 -5
  124. package/sdd/02_plan/07_integration/frontend_live_integration.md +0 -31
  125. package/sdd/02_plan/08_nonfunctional/README.md +0 -5
  126. package/sdd/02_plan/08_nonfunctional/repository_hygiene.md +0 -26
  127. package/sdd/02_plan/09_security/README.md +0 -5
  128. package/sdd/02_plan/10_test/README.md +0 -11
  129. package/sdd/02_plan/10_test/regression_verification.md +0 -39
  130. package/sdd/02_plan/10_test/templates/README.md +0 -8
  131. package/sdd/02_plan/10_test/templates/ui_parity_web_contract.template.yaml +0 -23
  132. package/sdd/02_plan/10_test/verification_strategy.md +0 -43
  133. package/sdd/02_plan/99_generated/from_planning/ui_parity/.gitkeep +0 -1
  134. package/sdd/02_plan/README.md +0 -40
  135. package/sdd/03_build/01_feature/README.md +0 -20
  136. package/sdd/03_build/01_feature/domain/README.md +0 -3
  137. package/sdd/03_build/01_feature/domain/account_and_access.md +0 -20
  138. package/sdd/03_build/01_feature/domain/catalog_and_inventory.md +0 -20
  139. package/sdd/03_build/01_feature/domain/ordering_and_fulfillment.md +0 -21
  140. package/sdd/03_build/01_feature/domain/support_and_observability.md +0 -21
  141. package/sdd/03_build/01_feature/domain_surfaces.md +0 -28
  142. package/sdd/03_build/01_feature/service/README.md +0 -3
  143. package/sdd/03_build/01_feature/service/admin_surface.md +0 -15
  144. package/sdd/03_build/01_feature/service/landing_surface.md +0 -13
  145. package/sdd/03_build/01_feature/service/mobile_surface.md +0 -14
  146. package/sdd/03_build/01_feature/service/web_surface.md +0 -14
  147. package/sdd/03_build/02_screen/README.md +0 -25
  148. package/sdd/03_build/02_screen/_screen_build_template.md +0 -26
  149. package/sdd/03_build/02_screen/admin/README.md +0 -5
  150. package/sdd/03_build/02_screen/landing/README.md +0 -5
  151. package/sdd/03_build/02_screen/mobile/README.md +0 -5
  152. package/sdd/03_build/02_screen/web/README.md +0 -5
  153. package/sdd/03_build/03_architecture/README.md +0 -10
  154. package/sdd/03_build/03_architecture/architecture_document_governance.md +0 -30
  155. package/sdd/03_build/03_architecture/build_ast_runtime_tree_governance.md +0 -24
  156. package/sdd/03_build/03_architecture/repository_governance.md +0 -18
  157. package/sdd/03_build/03_architecture/toolchain_governance.md +0 -36
  158. package/sdd/03_build/06_iac/README.md +0 -3
  159. package/sdd/03_build/06_iac/dev_runtime_delivery.md +0 -10
  160. package/sdd/03_build/06_iac/template_runtime_delivery.md +0 -49
  161. package/sdd/03_build/07_integration/README.md +0 -3
  162. package/sdd/03_build/07_integration/frontend_live_integration.md +0 -11
  163. package/sdd/03_build/08_nonfunctional/README.md +0 -3
  164. package/sdd/03_build/08_nonfunctional/repository_hygiene.md +0 -10
  165. package/sdd/03_build/10_test/README.md +0 -9
  166. package/sdd/03_build/10_test/regression_verification.md +0 -16
  167. package/sdd/03_build/10_test/verification_harness.md +0 -11
  168. package/sdd/03_build/README.md +0 -35
  169. package/sdd/03_verify/01_feature/README.md +0 -5
  170. package/sdd/03_verify/01_feature/domain_verification.md +0 -14
  171. package/sdd/03_verify/01_feature/service_verification.md +0 -22
  172. package/sdd/03_verify/02_screen/README.md +0 -6
  173. package/sdd/03_verify/02_screen/_screen_verify_template.md +0 -20
  174. package/sdd/03_verify/02_screen/admin/README.md +0 -4
  175. package/sdd/03_verify/02_screen/landing/README.md +0 -4
  176. package/sdd/03_verify/02_screen/mobile/README.md +0 -4
  177. package/sdd/03_verify/02_screen/web/README.md +0 -4
  178. package/sdd/03_verify/03_architecture/README.md +0 -10
  179. package/sdd/03_verify/03_architecture/architecture_document_governance.md +0 -15
  180. package/sdd/03_verify/03_architecture/build_ast_runtime_tree_governance.md +0 -28
  181. package/sdd/03_verify/03_architecture/repository_governance.md +0 -16
  182. package/sdd/03_verify/03_architecture/toolchain_governance.md +0 -58
  183. package/sdd/03_verify/06_iac/README.md +0 -3
  184. package/sdd/03_verify/06_iac/dev_runtime_delivery.md +0 -10
  185. package/sdd/03_verify/06_iac/template_runtime_delivery.md +0 -42
  186. package/sdd/03_verify/07_integration/README.md +0 -3
  187. package/sdd/03_verify/07_integration/frontend_live_integration.md +0 -16
  188. package/sdd/03_verify/08_nonfunctional/README.md +0 -3
  189. package/sdd/03_verify/08_nonfunctional/repository_hygiene.md +0 -14
  190. package/sdd/03_verify/10_test/README.md +0 -9
  191. package/sdd/03_verify/10_test/regression_verification.md +0 -16
  192. package/sdd/03_verify/10_test/ui_parity/README.md +0 -4
  193. package/sdd/03_verify/10_test/ui_parity/loop_runs/.gitkeep +0 -0
  194. package/sdd/03_verify/10_test/ui_parity/reference/.gitkeep +0 -0
  195. package/sdd/03_verify/10_test/ui_parity/staged_runs/.gitkeep +0 -0
  196. package/sdd/03_verify/10_test/verification_harness.md +0 -17
  197. package/sdd/03_verify/README.md +0 -22
  198. package/sdd/05_operate/01_runbooks/.gitkeep +0 -1
  199. package/sdd/05_operate/01_runbooks/README.md +0 -4
  200. package/sdd/05_operate/02_delivery_status/README.md +0 -4
  201. package/sdd/05_operate/02_delivery_status/service_status.md +0 -16
  202. package/sdd/05_operate/README.md +0 -12
  203. package/sdd/99_toolchain/01_automation/.gitkeep +0 -1
  204. package/sdd/99_toolchain/01_automation/README.md +0 -76
  205. package/sdd/99_toolchain/01_automation/agentic-dev/analyze_proof_results.py +0 -132
  206. package/sdd/99_toolchain/01_automation/agentic-dev/analyze_route_gap.py +0 -85
  207. package/sdd/99_toolchain/01_automation/agentic-dev/assets/repo-contract.template.json +0 -75
  208. package/sdd/99_toolchain/01_automation/agentic-dev/bootstrap_frontend_parity.sh +0 -84
  209. package/sdd/99_toolchain/01_automation/agentic-dev/init_frontend_parity.sh +0 -33
  210. package/sdd/99_toolchain/01_automation/agentic-dev/init_repo_contract.sh +0 -51
  211. package/sdd/99_toolchain/01_automation/agentic-dev/repo-contract.json +0 -76
  212. package/sdd/99_toolchain/01_automation/agentic-dev/resolve_frontend_target.py +0 -52
  213. package/sdd/99_toolchain/01_automation/agentic-dev/resolve_repo_contract.py +0 -56
  214. package/sdd/99_toolchain/01_automation/agentic-dev/run_frontend_target.sh +0 -100
  215. package/sdd/99_toolchain/01_automation/agentic-dev/run_repo_phase.sh +0 -140
  216. package/sdd/99_toolchain/01_automation/agentic-dev/validate_json_schema.py +0 -39
  217. package/sdd/99_toolchain/01_automation/agentic-parity-harness-design.md +0 -291
  218. package/sdd/99_toolchain/01_automation/assets/admin_screen_capture/dashboard.png +0 -0
  219. package/sdd/99_toolchain/01_automation/assets/admin_screen_capture/login.png +0 -0
  220. package/sdd/99_toolchain/01_automation/assets/admin_screen_capture/queue.png +0 -0
  221. package/sdd/99_toolchain/01_automation/assets/admin_screen_capture/support.png +0 -0
  222. package/sdd/99_toolchain/01_automation/assets/landing_screen_capture/home.png +0 -0
  223. package/sdd/99_toolchain/01_automation/assets/landing_screen_capture/login.png +0 -0
  224. package/sdd/99_toolchain/01_automation/assets/landing_screen_capture/workspace.png +0 -0
  225. package/sdd/99_toolchain/01_automation/assets/mobile_screen_capture/dashboard.png +0 -0
  226. package/sdd/99_toolchain/01_automation/assets/mobile_screen_capture/fulfillment.png +0 -0
  227. package/sdd/99_toolchain/01_automation/assets/mobile_screen_capture/login.png +0 -0
  228. package/sdd/99_toolchain/01_automation/assets/web_screen_capture/dashboard.png +0 -0
  229. package/sdd/99_toolchain/01_automation/assets/web_screen_capture/login.png +0 -0
  230. package/sdd/99_toolchain/01_automation/assets/web_screen_capture/orders.png +0 -0
  231. package/sdd/99_toolchain/01_automation/build_asset_recipes.py +0 -10
  232. package/sdd/99_toolchain/01_automation/build_screen_spec_pdf.py +0 -427
  233. package/sdd/99_toolchain/01_automation/capture_screen_assets.mjs +0 -148
  234. package/sdd/99_toolchain/01_automation/harness-layout.md +0 -34
  235. package/sdd/99_toolchain/01_automation/parity-execution-tooling-design.md +0 -319
  236. package/sdd/99_toolchain/01_automation/playwright_exactness_manifest.py +0 -21
  237. package/sdd/99_toolchain/01_automation/run_playwright_exactness.py +0 -87
  238. package/sdd/99_toolchain/01_automation/screen_spec_manifest.py +0 -321
  239. package/sdd/99_toolchain/01_automation/spec_asset_builder.py +0 -274
  240. package/sdd/99_toolchain/01_automation/ui-contract-projection.md +0 -79
  241. package/sdd/99_toolchain/01_automation/ui-parity/README.md +0 -60
  242. package/sdd/99_toolchain/01_automation/ui-parity/cli/extract-reference-pages.mjs +0 -2
  243. package/sdd/99_toolchain/01_automation/ui-parity/cli/materialize-reference-assets.mjs +0 -58
  244. package/sdd/99_toolchain/01_automation/ui-parity/cli/normalize-reference-assets.mjs +0 -2
  245. package/sdd/99_toolchain/01_automation/ui-parity/cli/route-gap-report.mjs +0 -187
  246. package/sdd/99_toolchain/01_automation/ui-parity/cli/run-proof.mjs +0 -50
  247. package/sdd/99_toolchain/01_automation/ui-parity/cli/scaffold-contract.mjs +0 -62
  248. package/sdd/99_toolchain/01_automation/ui-parity/cli/upload-parity1.mjs +0 -2
  249. package/sdd/99_toolchain/01_automation/ui-parity/contracts/collector-metadata.schema.json +0 -33
  250. package/sdd/99_toolchain/01_automation/ui-parity/contracts/proof-result.schema.json +0 -76
  251. package/sdd/99_toolchain/01_automation/ui-parity/contracts/route-gap-report.schema.json +0 -95
  252. package/sdd/99_toolchain/01_automation/ui-parity/core/capture-runner.mjs +0 -55
  253. package/sdd/99_toolchain/01_automation/ui-parity/core/load-adapter.mjs +0 -25
  254. package/sdd/99_toolchain/01_automation/ui-parity/core/load-contract.mjs +0 -81
  255. package/sdd/99_toolchain/01_automation/ui-parity/core/paths.mjs +0 -23
  256. package/sdd/99_toolchain/01_automation/ui-parity/core/proof-runner.mjs +0 -255
  257. package/sdd/99_toolchain/01_automation/ui-parity/interfaces/ui-parity-artifact-layout.md +0 -23
  258. package/sdd/99_toolchain/01_automation/ui-parity/interfaces/ui-parity-proof-interface.md +0 -60
  259. package/sdd/99_toolchain/01_automation/ui-parity/interfaces/ui-parity-route-gap-interface.md +0 -82
  260. package/sdd/99_toolchain/01_automation/ui-parity/runtime/playwright-runtime.mjs +0 -16
  261. package/sdd/99_toolchain/01_automation/ui-parity/runtime/static-runtime.mjs +0 -6
  262. package/sdd/99_toolchain/02_policies/.gitkeep +0 -1
  263. package/sdd/99_toolchain/02_policies/build-ast-governance-policy.md +0 -22
  264. package/sdd/99_toolchain/02_policies/compose-runtime-baseline-policy.md +0 -24
  265. package/sdd/99_toolchain/02_policies/convention-storage-policy.md +0 -26
  266. package/sdd/99_toolchain/02_policies/main-push-before-dev-deploy-policy.md +0 -27
  267. package/sdd/99_toolchain/02_policies/regression-verification-policy.md +0 -22
  268. package/sdd/99_toolchain/03_templates/.gitkeep +0 -1
  269. package/sdd/99_toolchain/03_templates/asset_recipe_manifest.example.py +0 -38
  270. package/sdd/99_toolchain/03_templates/generated_assets/README.md +0 -11
  271. package/sdd/99_toolchain/03_templates/generated_assets/example-brand-lockup.svg +0 -3
  272. package/sdd/99_toolchain/03_templates/generated_assets/example-brand-mark.svg +0 -3
  273. package/sdd/99_toolchain/03_templates/generated_assets/example-brand-wordmark.svg +0 -3
  274. package/sdd/99_toolchain/03_templates/playwright_exactness_manifest.example.py +0 -21
  275. package/sdd/99_toolchain/README.md +0 -23
  276. package/sdd/README.md +0 -21
@@ -1,427 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import atexit
5
- import shutil
6
- import subprocess
7
- import tempfile
8
- import textwrap
9
- from pathlib import Path
10
-
11
- from PIL import Image, ImageChops, ImageDraw, ImageFont
12
-
13
- from screen_spec_manifest import ROOT, SCREEN_MANIFESTS
14
-
15
-
16
- FONT_PATH = "NotoSansCJK-Regular.ttc"
17
- PAGE_SIZE = (2400, 1500)
18
- BG = "#f3f4f6"
19
- TEXT = "#111827"
20
- MUTED = "#6b7280"
21
- BORDER = "#d1d5db"
22
- PANEL = "#ffffff"
23
- PINK = "#ec4899"
24
- HEADER_FILL = "#e5e7eb"
25
- FIRST_INDEX_ROWS = 8
26
- NEXT_INDEX_ROWS = 12
27
- PDF_TEMP_DIR = Path(tempfile.mkdtemp(prefix="templates-screen-spec-"))
28
- PDF_CACHE: dict[tuple[str, int, int], Path] = {}
29
-
30
-
31
- def cleanup_tempdir() -> None:
32
- shutil.rmtree(PDF_TEMP_DIR, ignore_errors=True)
33
-
34
-
35
- atexit.register(cleanup_tempdir)
36
-
37
-
38
- def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
39
- index = 1 if bold else 0
40
- return ImageFont.truetype(FONT_PATH, size=size, index=index)
41
-
42
-
43
- def wrap(text: str, width: int) -> list[str]:
44
- return textwrap.wrap(text, width=width, break_long_words=False, break_on_hyphens=False) or [text]
45
-
46
-
47
- def parse_viewport_size(value: str) -> tuple[int, int]:
48
- width, height = value.lower().split("x", 1)
49
- return int(width), int(height)
50
-
51
-
52
- def fit_image(image: Image.Image, width: int, height: int) -> tuple[Image.Image, tuple[int, int]]:
53
- source = image.copy()
54
- source.thumbnail((width, height))
55
- x = (width - source.width) // 2
56
- y = (height - source.height) // 2
57
- return source, (x, y)
58
-
59
-
60
- def trim_background(image: Image.Image) -> Image.Image:
61
- background = Image.new(image.mode, image.size, image.getpixel((0, 0)))
62
- diff = ImageChops.difference(image, background)
63
- bbox = diff.getbbox()
64
- if not bbox:
65
- return image
66
-
67
- left, top, right, bottom = bbox
68
- padding = 12
69
- return image.crop(
70
- (
71
- max(0, left - padding),
72
- max(0, top - padding),
73
- min(image.width, right + padding),
74
- min(image.height, bottom + padding),
75
- )
76
- )
77
-
78
-
79
- def trim_dark_border(image: Image.Image, threshold: int = 12) -> Image.Image:
80
- grayscale = image.convert("L")
81
- mask = grayscale.point(lambda value: 255 if value > threshold else 0)
82
- bbox = mask.getbbox()
83
- if not bbox:
84
- return image
85
- return image.crop(bbox)
86
-
87
-
88
- def resolve_path(path: str | Path) -> Path:
89
- candidate = Path(path)
90
- if candidate.is_absolute():
91
- return candidate
92
- return ROOT / candidate
93
-
94
-
95
- def transform_callouts_for_crop(
96
- callouts: list[tuple[tuple[float, float], str, str]],
97
- image_size: tuple[int, int],
98
- crop_box: tuple[int, int, int, int],
99
- ) -> list[tuple[tuple[float, float], str, str]]:
100
- image_width, image_height = image_size
101
- left, top, right, bottom = crop_box
102
- crop_width = right - left
103
- crop_height = bottom - top
104
- transformed: list[tuple[tuple[float, float], str, str]] = []
105
-
106
- for anchor, title, desc in callouts:
107
- px = anchor[0] * image_width
108
- py = anchor[1] * image_height
109
- if left <= px <= right and top <= py <= bottom:
110
- transformed.append((((px - left) / crop_width, (py - top) / crop_height), title, desc))
111
-
112
- return transformed
113
-
114
-
115
- def auto_split_tall_screen(service_config: dict, screen: dict, screenshot: Image.Image) -> list[dict]:
116
- capture_policy = service_config.get("capture_policy") or {}
117
- viewport_width, viewport_height = parse_viewport_size(capture_policy.get("default_viewport", "1690x940"))
118
- segment_height = round(screenshot.width * viewport_height / viewport_width)
119
- if segment_height <= 0 or segment_height >= screenshot.height:
120
- return [screen]
121
-
122
- slices: list[tuple[int, int]] = []
123
- for anchor, _, _ in screen["callouts"]:
124
- center_y = round(anchor[1] * screenshot.height)
125
- top = max(0, min(center_y - segment_height // 2, screenshot.height - segment_height))
126
- bottom = min(screenshot.height, top + segment_height)
127
- if bottom - top < segment_height:
128
- top = max(0, bottom - segment_height)
129
- if slices and top <= slices[-1][1] - 120:
130
- prev_top, prev_bottom = slices[-1]
131
- slices[-1] = (prev_top, max(prev_bottom, bottom))
132
- else:
133
- slices.append((top, bottom))
134
-
135
- expanded: list[dict] = []
136
- total = len(slices)
137
- for index, (top, bottom) in enumerate(slices, start=1):
138
- crop_box = (0, top, screenshot.width, bottom)
139
- transformed_callouts = transform_callouts_for_crop(screen["callouts"], screenshot.size, crop_box)
140
- if not transformed_callouts:
141
- continue
142
- detail_screen = dict(screen)
143
- detail_screen["name"] = f"{screen['name']} ({index}/{total})"
144
- detail_screen["__image"] = screenshot.crop(crop_box)
145
- detail_screen["callouts"] = transformed_callouts
146
- expanded.append(detail_screen)
147
-
148
- return expanded or [screen]
149
-
150
-
151
- def chunk_screens(screens: list[dict]) -> list[list[dict]]:
152
- chunks: list[list[dict]] = []
153
- first = FIRST_INDEX_ROWS if screens else 0
154
- if first:
155
- chunks.append(screens[:first])
156
- rest = screens[first:]
157
- while rest:
158
- chunks.append(rest[:NEXT_INDEX_ROWS])
159
- rest = rest[NEXT_INDEX_ROWS:]
160
- return chunks or [[]]
161
-
162
-
163
- def draw_table_pages(config: dict) -> list[Image.Image]:
164
- pages: list[Image.Image] = []
165
- screen_chunks = chunk_screens(config["screens"])
166
-
167
- for page_index, chunk in enumerate(screen_chunks, start=1):
168
- page = Image.new("RGB", PAGE_SIZE, BG)
169
- draw = ImageDraw.Draw(page)
170
- draw.rounded_rectangle((50, 40, PAGE_SIZE[0] - 50, PAGE_SIZE[1] - 40), radius=28, fill=PANEL, outline=BORDER, width=2)
171
-
172
- title = config["title"]
173
- if len(screen_chunks) > 1:
174
- title = f"{title} ({page_index}/{len(screen_chunks)})"
175
- draw.text((100, 84), title, font=font(56, bold=True), fill=TEXT)
176
- meta_y = 166
177
- draw.text((100, meta_y), "문서 상태: current", font=font(26), fill=MUTED)
178
- draw.text((392, meta_y), "작성 버전: 1.0.0", font=font(26), fill=MUTED)
179
- draw.text((704, meta_y), f"대상 서비스: {config['service_label']}", font=font(26), fill=MUTED)
180
-
181
- top = 240
182
- if page_index == 1 and config.get("source_refs"):
183
- draw.text((100, 226), "기준 산출물", font=font(28, bold=True), fill=TEXT)
184
- ref_y = 268
185
- for ref in config["source_refs"]:
186
- label = ref.get("label", "source")
187
- path = ref.get("path", "")
188
- for line_index, line in enumerate(wrap(f"- {label}: {path}", 84)):
189
- prefix = "" if line_index == 0 else " "
190
- draw.text((120, ref_y), f"{prefix}{line}", font=font(22), fill=MUTED)
191
- ref_y += 28
192
- top = max(top, ref_y + 24)
193
-
194
- left = 100
195
- right = PAGE_SIZE[0] - 100
196
- columns = [
197
- ("화면코드", 300),
198
- ("화면명", 500),
199
- ("주요 경로", 340),
200
- ("관련 기능", right - left - 300 - 500 - 340),
201
- ]
202
-
203
- x = left
204
- for label, width in columns:
205
- draw.rectangle((x, top, x + width, top + 72), fill=HEADER_FILL, outline=BORDER)
206
- draw.text((x + 16, top + 18), label, font=font(28, bold=True), fill=TEXT)
207
- x += width
208
-
209
- y = top + 72
210
- for item in chunk:
211
- x = left
212
- values = [item["code"], item["name"], item["route"], ", ".join(item["features"])]
213
- row_height = 84
214
- for idx, (_, width) in enumerate(columns):
215
- draw.rectangle((x, y, x + width, y + row_height), fill=PANEL, outline=BORDER)
216
- wrapped = wrap(values[idx], 16 if idx < 3 else 24)
217
- ty = y + 12
218
- for line in wrapped[:2]:
219
- draw.text((x + 16, ty), line, font=font(22), fill=TEXT)
220
- ty += 28
221
- x += width
222
- y += row_height
223
-
224
- if page_index == len(screen_chunks):
225
- draw.text((100, PAGE_SIZE[1] - 100), config["cover_note"], font=font(24), fill=MUTED)
226
- pages.append(page)
227
-
228
- return pages
229
-
230
-
231
- def draw_callout(draw: ImageDraw.ImageDraw, x: int, y: int, label: int) -> None:
232
- radius = 30
233
- draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=PINK, outline="#ffffff", width=4)
234
- draw.text((x, y), str(label), font=font(30, bold=True), fill="#ffffff", anchor="mm")
235
-
236
-
237
- def extract_pdf_page(source_path: Path, page: int, dpi: int) -> Path:
238
- key = (str(source_path), page, dpi)
239
- if key in PDF_CACHE:
240
- return PDF_CACHE[key]
241
-
242
- output_prefix = PDF_TEMP_DIR / f"{source_path.stem}-p{page}-{dpi}"
243
- subprocess.run(
244
- [
245
- "pdftoppm",
246
- "-png",
247
- "-r",
248
- str(dpi),
249
- "-f",
250
- str(page),
251
- "-l",
252
- str(page),
253
- str(source_path),
254
- str(output_prefix),
255
- ],
256
- check=True,
257
- stdout=subprocess.PIPE,
258
- stderr=subprocess.PIPE,
259
- text=True,
260
- )
261
- candidates = sorted(output_prefix.parent.glob(f"{output_prefix.name}-*.png"))
262
- if not candidates:
263
- raise FileNotFoundError(f"failed to extract page {page} from {source_path}")
264
- output_path = candidates[0]
265
- PDF_CACHE[key] = output_path
266
- return output_path
267
-
268
-
269
- def load_screen_image(service_config: dict, screen: dict) -> Image.Image:
270
- if "__image" in screen:
271
- return screen["__image"].copy()
272
-
273
- asset_path: Path
274
- source = screen.get("source")
275
- if source:
276
- source_type = source["type"]
277
- if source_type == "pdf_page":
278
- source_path = resolve_path(source["path"])
279
- asset_path = extract_pdf_page(source_path, int(source["page"]), int(source.get("dpi", 144)))
280
- elif source_type == "image":
281
- asset_path = resolve_path(source["path"])
282
- else:
283
- raise ValueError(f"unsupported source type: {source_type}")
284
- else:
285
- asset_dir = resolve_path(service_config["asset_dir"])
286
- asset_path = asset_dir / screen["asset"]
287
-
288
- if not asset_path.exists():
289
- raise FileNotFoundError(f"missing source asset: {asset_path}")
290
-
291
- screenshot = Image.open(asset_path).convert("RGB")
292
- crop_box = screen.get("crop_box")
293
- if crop_box:
294
- left, top, right, bottom = crop_box
295
- bounded_crop = (
296
- max(0, min(int(left), screenshot.width)),
297
- max(0, min(int(top), screenshot.height)),
298
- max(0, min(int(right), screenshot.width)),
299
- max(0, min(int(bottom), screenshot.height)),
300
- )
301
- if bounded_crop[2] > bounded_crop[0] and bounded_crop[3] > bounded_crop[1]:
302
- screenshot = screenshot.crop(bounded_crop)
303
-
304
- if source and source["type"] == "pdf_page":
305
- screenshot = trim_dark_border(screenshot)
306
- if service_config.get("trim_background"):
307
- screenshot = trim_background(screenshot)
308
- return screenshot
309
-
310
-
311
- def draw_detail_page(service_config: dict, screen: dict) -> Image.Image:
312
- page = Image.new("RGB", PAGE_SIZE, BG)
313
- draw = ImageDraw.Draw(page)
314
- draw.text((60, 46), f"{screen['code']} {screen['name']}", font=font(48, bold=True), fill=TEXT)
315
- draw.text((60, 112), f"경로: {screen['route']}", font=font(24), fill=MUTED)
316
- draw.text((300, 112), f"접근: {screen['access']}", font=font(24), fill=MUTED)
317
- draw.text((510, 112), f"관련 기능: {', '.join(screen['features'])}", font=font(24), fill=MUTED)
318
-
319
- screenshot = load_screen_image(service_config, screen)
320
- image_region = (50, 170, 1560, 1420)
321
- max_inner_width = image_region[2] - image_region[0] - 50
322
- max_inner_height = image_region[3] - image_region[1] - 50
323
- screenshot_fit, _ = fit_image(screenshot, max_inner_width, max_inner_height)
324
-
325
- panel_width = screenshot_fit.width + 50
326
- panel_height = screenshot_fit.height + 50
327
- image_panel = (
328
- image_region[0] + (image_region[2] - image_region[0] - panel_width) // 2,
329
- image_region[1] + (image_region[3] - image_region[1] - panel_height) // 2,
330
- image_region[0] + (image_region[2] - image_region[0] - panel_width) // 2 + panel_width,
331
- image_region[1] + (image_region[3] - image_region[1] - panel_height) // 2 + panel_height,
332
- )
333
- table_panel = (1600, 170, 2340, 1420)
334
- draw.rounded_rectangle(image_panel, radius=28, fill=PANEL, outline=BORDER, width=2)
335
- draw.rounded_rectangle(table_panel, radius=28, fill=PANEL, outline=BORDER, width=2)
336
-
337
- screenshot_x = image_panel[0] + 25
338
- screenshot_y = image_panel[1] + 25
339
- page.paste(screenshot_fit, (screenshot_x, screenshot_y))
340
-
341
- overlay = ImageDraw.Draw(page)
342
- for idx, (anchor, _, _) in enumerate(screen["callouts"], start=1):
343
- callout_x = int(screenshot_x + screenshot_fit.width * anchor[0])
344
- callout_y = int(screenshot_y + screenshot_fit.height * anchor[1])
345
- draw_callout(overlay, callout_x, callout_y, idx)
346
-
347
- header_height = 76
348
- draw.rectangle((table_panel[0], table_panel[1], table_panel[2], table_panel[1] + header_height), fill=HEADER_FILL, outline=BORDER)
349
- draw.text((table_panel[0] + 20, table_panel[1] + 20), "화면명", font=font(28, bold=True), fill=TEXT)
350
- draw.text((table_panel[0] + 170, table_panel[1] + 20), f"{screen['name']} ({screen['code']})", font=font(28, bold=True), fill=TEXT)
351
-
352
- left_col = 96
353
- row_y = table_panel[1] + header_height
354
- for idx, (_, title, desc) in enumerate(screen["callouts"], start=1):
355
- lines = wrap(desc, 22)
356
- row_height = 56 + max(1, len(lines)) * 36
357
- draw.rectangle((table_panel[0], row_y, table_panel[0] + left_col, row_y + row_height), fill=PANEL, outline=BORDER)
358
- draw.rectangle((table_panel[0] + left_col, row_y, table_panel[2], row_y + row_height), fill=PANEL, outline=BORDER)
359
- draw.text((table_panel[0] + left_col / 2, row_y + row_height / 2), str(idx), font=font(28, bold=True), fill=TEXT, anchor="mm")
360
- draw.text((table_panel[0] + left_col + 18, row_y + 14), title, font=font(28, bold=True), fill=TEXT)
361
- text_y = row_y + 54
362
- for line_index, line in enumerate(lines):
363
- prefix = "- " if line_index == 0 else " "
364
- draw.text((table_panel[0] + left_col + 18, text_y), f"{prefix}{line}", font=font(24), fill=TEXT)
365
- text_y += 34
366
- row_y += row_height
367
-
368
- return page
369
-
370
-
371
- def iter_detail_screens(screen: dict) -> list[dict]:
372
- segments = screen.get("segments")
373
- if not segments:
374
- return [screen]
375
-
376
- expanded: list[dict] = []
377
- for segment in segments:
378
- detail_screen = dict(screen)
379
- detail_screen["name"] = f"{screen['name']} ({segment['label']})"
380
- detail_screen["crop_box"] = segment["crop_box"]
381
- detail_screen["callouts"] = segment["callouts"]
382
- expanded.append(detail_screen)
383
- return expanded
384
-
385
-
386
- def expand_detail_screens(service_config: dict, screen: dict) -> list[dict]:
387
- explicit_segments = iter_detail_screens(screen)
388
- if len(explicit_segments) > 1 or screen.get("segments"):
389
- return explicit_segments
390
-
391
- screenshot = load_screen_image(service_config, screen)
392
- if screenshot.height > screenshot.width:
393
- return auto_split_tall_screen(service_config, screen, screenshot)
394
- return [screen]
395
-
396
-
397
- def build_service(service: str) -> Path:
398
- config = SCREEN_MANIFESTS[service]
399
- pages = draw_table_pages(config)
400
- for screen in config["screens"]:
401
- for detail_screen in expand_detail_screens(config, screen):
402
- pages.append(draw_detail_page(config, detail_screen))
403
-
404
- output = resolve_path(config["output"])
405
- output.parent.mkdir(parents=True, exist_ok=True)
406
- first, *rest = pages
407
- first.save(output, "PDF", resolution=220.0, save_all=True, append_images=rest)
408
- return output
409
-
410
-
411
- def parse_args() -> argparse.Namespace:
412
- parser = argparse.ArgumentParser(description="Build screen spec PDFs from the manifest-defined capture assets.")
413
- parser.add_argument("--service", default="all", choices=["all", *sorted(SCREEN_MANIFESTS.keys())], help="Build a single service or all services.")
414
- parser.add_argument("--all", action="store_true", help="Build all supported service screen spec PDFs.")
415
- return parser.parse_args()
416
-
417
-
418
- def main() -> None:
419
- args = parse_args()
420
- targets = sorted(SCREEN_MANIFESTS.keys()) if args.all or args.service == "all" else [args.service]
421
- for service in targets:
422
- output = build_service(service)
423
- print(output)
424
-
425
-
426
- if __name__ == "__main__":
427
- main()
@@ -1,148 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs/promises";
4
- import { createRequire } from "node:module";
5
- import path from "node:path";
6
-
7
- const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../..");
8
- const VIEWPORT = { width: 1690, height: 940 };
9
- const require = createRequire(import.meta.url);
10
-
11
- function loadPlaywright() {
12
- const resolutionRoots = [
13
- ROOT,
14
- path.join(ROOT, "client/web"),
15
- path.join(ROOT, "client/admin"),
16
- path.join(ROOT, "client/mobile"),
17
- path.join(ROOT, "client/landing"),
18
- ];
19
-
20
- for (const base of resolutionRoots) {
21
- try {
22
- const resolved = require.resolve("playwright", { paths: [base] });
23
- return require(resolved);
24
- } catch {
25
- // Try the next workspace root.
26
- }
27
- }
28
-
29
- throw new Error("Unable to resolve `playwright` from repo root or frontend workspaces.");
30
- }
31
-
32
- const { chromium } = loadPlaywright();
33
-
34
- const manifests = {
35
- web: {
36
- baseUrl: "http://127.0.0.1:3001",
37
- apiBase: "http://127.0.0.1:8000/api/v1",
38
- storageKey: "web.auth.token",
39
- credentials: { email: "admin@example.com", password: "<CHANGE_ME>" },
40
- assetDir: path.join(ROOT, "sdd/99_toolchain/01_automation/assets/web_screen_capture"),
41
- screens: [
42
- { route: "/login", asset: "login.png", requiresAuth: false },
43
- { route: "/", asset: "dashboard.png", requiresAuth: true },
44
- { route: "/orders", asset: "orders.png", requiresAuth: true },
45
- ],
46
- },
47
- admin: {
48
- baseUrl: "http://127.0.0.1:4000",
49
- apiBase: "http://127.0.0.1:8000/api/v1",
50
- storageKey: "admin.auth.token",
51
- credentials: { email: "admin@example.com", password: "<CHANGE_ME>" },
52
- assetDir: path.join(ROOT, "sdd/99_toolchain/01_automation/assets/admin_screen_capture"),
53
- screens: [
54
- { route: "/login", asset: "login.png", requiresAuth: false },
55
- { route: "/", asset: "dashboard.png", requiresAuth: true },
56
- { route: "/queue", asset: "queue.png", requiresAuth: true },
57
- { route: "/support", asset: "support.png", requiresAuth: true },
58
- ],
59
- },
60
- mobile: {
61
- baseUrl: "http://127.0.0.1:3002",
62
- apiBase: "http://127.0.0.1:8000/api/v1",
63
- storageKey: "mobile.auth.token",
64
- credentials: { email: "operator@example.com", password: "<CHANGE_ME>" },
65
- assetDir: path.join(ROOT, "sdd/99_toolchain/01_automation/assets/mobile_screen_capture"),
66
- screens: [
67
- { route: "/login", asset: "login.png", requiresAuth: false },
68
- { route: "/", asset: "dashboard.png", requiresAuth: true },
69
- { route: "/fulfillment", asset: "fulfillment.png", requiresAuth: true },
70
- ],
71
- },
72
- landing: {
73
- baseUrl: "http://127.0.0.1:3000",
74
- apiBase: "http://127.0.0.1:8000/api/v1",
75
- storageKey: "landing.auth.token",
76
- credentials: { email: "admin@example.com", password: "<CHANGE_ME>" },
77
- assetDir: path.join(ROOT, "sdd/99_toolchain/01_automation/assets/landing_screen_capture"),
78
- screens: [
79
- { route: "/", asset: "home.png", requiresAuth: false },
80
- { route: "/login", asset: "login.png", requiresAuth: false },
81
- { route: "/workspace", asset: "workspace.png", requiresAuth: true },
82
- ],
83
- },
84
- };
85
-
86
- async function login(apiBase, credentials) {
87
- const response = await fetch(`${apiBase}/auth/login`, {
88
- method: "POST",
89
- headers: { "Content-Type": "application/json" },
90
- body: JSON.stringify(credentials),
91
- });
92
- if (!response.ok) {
93
- throw new Error(`login failed: ${response.status} ${response.statusText}`);
94
- }
95
- return response.json();
96
- }
97
-
98
- async function captureService(service) {
99
- const manifest = manifests[service];
100
- if (!manifest) {
101
- throw new Error(`unknown service: ${service}`);
102
- }
103
-
104
- await fs.mkdir(manifest.assetDir, { recursive: true });
105
- const token = await login(manifest.apiBase, manifest.credentials);
106
- const browser = await chromium.launch({ headless: true });
107
- const context = await browser.newContext({ viewport: VIEWPORT });
108
-
109
- try {
110
- for (const screen of manifest.screens) {
111
- const page = await context.newPage();
112
- await page.addInitScript(
113
- ({ storageKey, accessToken, requiresAuth }) => {
114
- if (requiresAuth) {
115
- window.localStorage.setItem(storageKey, JSON.stringify(accessToken));
116
- return;
117
- }
118
- window.localStorage.removeItem(storageKey);
119
- },
120
- { storageKey: manifest.storageKey, accessToken: token, requiresAuth: screen.requiresAuth },
121
- );
122
- await page.goto(`${manifest.baseUrl}${screen.route}`, { waitUntil: "networkidle" });
123
- await page.waitForTimeout(300);
124
- await page.screenshot({
125
- path: path.join(manifest.assetDir, screen.asset),
126
- fullPage: true,
127
- });
128
- await page.close();
129
- }
130
- } finally {
131
- await context.close();
132
- await browser.close();
133
- }
134
- }
135
-
136
- async function main() {
137
- const service = process.argv[2];
138
- const targets = !service || service === "all" ? Object.keys(manifests) : [service];
139
- for (const target of targets) {
140
- await captureService(target);
141
- console.log(`captured: ${target}`);
142
- }
143
- }
144
-
145
- main().catch((error) => {
146
- console.error(error);
147
- process.exit(1);
148
- });
@@ -1,34 +0,0 @@
1
- # Harness Layout
2
-
3
- 이 저장소의 Claude/Codex 하네스는 다음 위치를 기준으로 유지한다.
4
-
5
- - `.claude/`: Claude 설정, 워크스페이스 가이드
6
- - `.codex/`: Codex 설정, generic 에이전트 역할, 스킬
7
-
8
- ## Purpose
9
-
10
- - 새 템플릿 저장소를 만들 때 바로 복사 가능한 generic 하네스 표면을 제공한다.
11
- - 정책 문서(`01_policies`)와 실제 실행 자산(`.claude`, `.codex`)의 연결점을 명시한다.
12
-
13
- ## Current Contents
14
-
15
- - `.claude/CLAUDE.md`
16
- - `.claude/settings*.json`
17
- - `.codex/config.toml`
18
- - `.codex/agents/*.toml`
19
- - `.codex/skills/*`
20
- - `agentic-parity-harness-design.md`
21
- - `parity-execution-tooling-design.md`
22
-
23
- ## Extension Point
24
-
25
- - UI parity와 `agentic-dev` 강제 흐름은 [agentic-parity-harness-design.md](agentic-parity-harness-design.md)에 정의된 계약을 따른다.
26
- - parity 실행 도구 자체의 소유 위치와 계층 분리는 [parity-execution-tooling-design.md](parity-execution-tooling-design.md)를 따른다.
27
- - 프론트 템플릿 복제 직후에는 `bash sdd/99_toolchain/01_automation/agentic-dev/init_frontend_parity.sh . web`을 먼저 실행해 repo contract와 route-gap/generated parity 기초 자산을 만든다.
28
- - `mobile`도 같은 parity target 규약을 따르며 `run_frontend_target.sh ... mobile`으로 동일하게 실행한다.
29
- - 첫 실행 검증은 `bash sdd/99_toolchain/01_automation/agentic-dev/bootstrap_frontend_parity.sh . web`으로 route-gap gate와 proof gate까지 닫는다.
30
-
31
- ## Sanitization Rules
32
-
33
- - 특정 서비스명, 회사명, 내부 도메인, 실환경 URL, 자격증명, 브라우저 프로필, 실행 산출물은 저장하지 않는다.
34
- - 브라우저 자동화 skill에는 reusable source만 포함하고 `tmp/`, `profiles/`, `node_modules/` 같은 실행 산출물은 제외한다.