@wix/agent-skills 1.15.0 → 1.16.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 (355) hide show
  1. package/README.md +5 -1
  2. package/package.json +1 -1
  3. package/skills/wix-app/SKILL.md +4 -1
  4. package/skills/wix-app/references/APP_TOOLS.md +134 -0
  5. package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +1 -1
  6. package/skills/wix-app/references/DASHBOARD_PAGE.md +0 -10
  7. package/skills/wix-app/references/DATA_COLLECTION.md +6 -4
  8. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +28 -7
  9. package/skills/wix-app/references/SERVICE_PLUGIN.md +12 -10
  10. package/skills/wix-app/references/STORES_VERSIONING.md +9 -211
  11. package/skills/wix-app/references/app-tools/TOOLS.md +75 -0
  12. package/skills/wix-app/references/backend-event/COMMON-EVENTS.md +5 -0
  13. package/skills/wix-app/references/dashboard-page/DASHBOARD_API.md +2 -2
  14. package/skills/wix-app/references/editor-react-component/A11Y-REVIEW-REFERENCE.md +172 -0
  15. package/skills/wix-app/references/editor-react-component/A11Y-REVIEW.md +138 -0
  16. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +172 -0
  17. package/skills/wix-app/references/editor-react-component/BRANDED-COMPONENTS.md +29 -0
  18. package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +1 -1
  19. package/skills/wix-app/references/editor-react-component/COMPONENT-PREVIEW.md +57 -0
  20. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +33 -2
  21. package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +5 -3
  22. package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +18 -1
  23. package/skills/wix-app/references/editor-react-component/PARTS.md +15 -2
  24. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +4 -3
  25. package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +13 -9
  26. package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +4 -0
  27. package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
  28. package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +3 -1
  29. package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +4 -0
  30. package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +54 -0
  31. package/skills/wix-app/references/stores/CATEGORIES.md +22 -0
  32. package/skills/wix-app/references/stores/GET_PRODUCT.md +99 -0
  33. package/skills/wix-app/references/stores/INVENTORY.md +37 -0
  34. package/skills/wix-app/references/stores/QUERY.md +119 -0
  35. package/skills/wix-app/references/stores/WRITE.md +55 -0
  36. package/skills/wix-docs/SKILL.md +35 -3
  37. package/skills/wix-headless/SKILL.md +9 -2
  38. package/skills/wix-headless/entry/bootstrap.mjs +2 -2
  39. package/skills/wix-headless/entry/skill.md +0 -2
  40. package/skills/wix-headless/references/CAPABILITIES.md +6 -4
  41. package/skills/wix-headless/references/CONTENT.md +36 -0
  42. package/skills/wix-headless/references/DESIGN.md +54 -0
  43. package/skills/wix-headless/references/DISCOVERY.md +25 -1
  44. package/skills/wix-headless/references/FEEDBACK.md +16 -5
  45. package/skills/wix-headless/references/IMAGE_GENERATION.md +10 -0
  46. package/skills/wix-headless/references/SDK_HANDOFF.md +2 -0
  47. package/skills/wix-headless/references/SEED.md +3 -1
  48. package/skills/wix-headless/references/SETUP.md +1 -1
  49. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +3 -2
  50. package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-reservations.md +42 -2
  51. package/skills/wix-headless/references/inline-recipes/setup-bookings.md +5 -1
  52. package/skills/wix-headless/references/inline-recipes/setup-forms.md +1 -1
  53. package/skills/wix-headless/references/inline-recipes/setup-online-store.md +33 -8
  54. package/skills/wix-headless/references/inline-recipes/setup-portfolio.md +3 -1
  55. package/skills/wix-headless/references/inline-recipes/setup-restaurant-experiences.md +73 -0
  56. package/skills/wix-headless/references/inline-recipes/setup-restaurant-orders.md +38 -2
  57. package/skills/wix-headless/references/inline-recipes/setup-restaurant-reservations.md +20 -1
  58. package/skills/wix-headless/references/inline-recipes/setup-restaurants.md +1 -1
  59. package/skills/wix-headless/references/managed/CONNECT.md +2 -2
  60. package/skills/wix-headless/references/managed/CREATE.md +15 -1
  61. package/skills/wix-headless/references/managed/DEPLOYMENT.md +47 -0
  62. package/skills/wix-headless/references/self-managed/DEPLOYMENT.md +8 -0
  63. package/skills/wix-headless/references/stripe/DEPLOYMENT.md +8 -0
  64. package/skills/wix-manage/SKILL.md +131 -12
  65. package/skills/wix-manage/references/analytics/analytics-dashboard-navigation.md +41 -0
  66. package/skills/wix-manage/references/analytics/query-site-analytics.md +66 -9
  67. package/skills/wix-manage/references/app-installation/app-installation-dashboard-navigation.md +28 -0
  68. package/skills/wix-manage/references/app-installation/install-wix-apps.md +3 -0
  69. package/skills/wix-manage/references/blog/blog-dashboard-navigation.md +56 -0
  70. package/skills/wix-manage/references/blog/how-to-create-blog-posts.md +31 -6
  71. package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +3 -1
  72. package/skills/wix-manage/references/bookings/bookings-dashboard-navigation.md +63 -0
  73. package/skills/wix-manage/references/bookings/create-booking-service-from-prompt.md +6 -5
  74. package/skills/wix-manage/references/bookings/create-course-service.md +30 -5
  75. package/skills/wix-manage/references/bookings/diagnose-availability-issues.md +85 -42
  76. package/skills/wix-manage/references/cms/cms-dashboard-navigation.md +44 -0
  77. package/skills/wix-manage/references/cms/cms-data-items-crud.md +31 -3
  78. package/skills/wix-manage/references/cms/cms-publishing-flow.md +287 -0
  79. package/skills/wix-manage/references/cms/cms-schema-management.md +33 -1
  80. package/skills/wix-manage/references/contacts/contacts-dashboard-navigation.md +46 -0
  81. package/skills/wix-manage/references/contacts/create-a-contact.md +82 -0
  82. package/skills/wix-manage/references/contacts/update-a-contact.md +119 -0
  83. package/skills/wix-manage/references/dashboard-navigation/dashboard-navigation.md +82 -0
  84. package/skills/wix-manage/references/dashboard-navigation/routes.json +1733 -0
  85. package/skills/wix-manage/references/domains/domains-dashboard-navigation.md +32 -0
  86. package/skills/wix-manage/references/ecommerce/ecom-pricing.md +4 -4
  87. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  88. package/skills/wix-manage/references/ecommerce/gift-cards/ecom-gift-cards-goal-sell-gift-cards.md +338 -0
  89. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-create-discount-rule.md +42 -78
  90. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-bundle-and-save.md +1 -1
  91. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-stock-mover.md +1 -1
  92. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-upsell-boost.md +1 -1
  93. package/skills/wix-manage/references/ecommerce/recommend-ecommerce-strategy.md +90 -92
  94. package/skills/wix-manage/references/events/create-wix-event.md +176 -0
  95. package/skills/wix-manage/references/events/manage-wix-events.md +174 -0
  96. package/skills/wix-manage/references/forms/forms-dashboard-navigation.md +44 -0
  97. package/skills/wix-manage/references/get-paid/get-paid-dashboard-navigation.md +52 -0
  98. package/skills/wix-manage/references/get-paid/how-to-setup-wix-payments.md +30 -7
  99. package/skills/wix-manage/references/google-ads/billing-and-payment.md +80 -0
  100. package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +212 -0
  101. package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +110 -0
  102. package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +25 -0
  103. package/skills/wix-manage/references/google-ads/install-and-create-account.md +155 -0
  104. package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +63 -0
  105. package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +155 -0
  106. package/skills/wix-manage/references/marketing/create-and-publish-social-post.md +225 -144
  107. package/skills/wix-manage/references/marketing/generate-and-publish-marketing-plan.md +19 -10
  108. package/skills/wix-manage/references/marketing/marketing-dashboard-navigation.md +52 -0
  109. package/skills/wix-manage/references/pricing-plans/create-and-update-pricing-plans.md +99 -1
  110. package/skills/wix-manage/references/pricing-plans/pricing-plans-dashboard-navigation.md +45 -0
  111. package/skills/wix-manage/references/restaurants/restaurants-dashboard-navigation.md +68 -0
  112. package/skills/wix-manage/references/restaurants/wix-restaurants-setup.md +94 -125
  113. package/skills/wix-manage/references/site-properties/change-payment-currency-site-properties.md +48 -9
  114. package/skills/wix-manage/references/site-properties/site-properties-dashboard-navigation.md +31 -0
  115. package/skills/wix-manage/references/sites/create-headless-site.md +52 -0
  116. package/skills/wix-manage/references/sites/create-site-from-template.md +2 -25
  117. package/skills/wix-manage/references/sites/site-import.md +243 -0
  118. package/skills/wix-manage/references/sites/sites-dashboard-navigation.md +29 -0
  119. package/skills/wix-manage/references/stores/add-store-pages-to-site.md +17 -1
  120. package/skills/wix-manage/references/stores/create-product-from-image.md +8 -6
  121. package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +16 -10
  122. package/skills/wix-manage/references/stores/find-products-query-and-search-catalog-v3.md +26 -8
  123. package/skills/wix-manage/references/stores/stores-dashboard-navigation.md +65 -0
  124. package/skills/wix-manage/references/stores/update-product-with-options.md +39 -7
  125. package/skills/wix-replatform/.publish-manifest.json +8 -0
  126. package/skills/wix-replatform/SKILL.md +693 -0
  127. package/skills/wix-replatform/lib/artifact-freshness.js +215 -0
  128. package/skills/wix-replatform/lib/completion-report.js +200 -0
  129. package/skills/wix-replatform/lib/config-env.js +59 -0
  130. package/skills/wix-replatform/lib/execution-manifest.js +185 -0
  131. package/skills/wix-replatform/lib/execution-state.js +173 -0
  132. package/skills/wix-replatform/lib/import-recovery.js +313 -0
  133. package/skills/wix-replatform/lib/local-state.js +642 -0
  134. package/skills/wix-replatform/lib/orchestration-preflight.js +143 -0
  135. package/skills/wix-replatform/lib/orchestration-router.js +122 -0
  136. package/skills/wix-replatform/lib/orchestration-state.js +455 -0
  137. package/skills/wix-replatform/lib/progress-log.js +224 -0
  138. package/skills/wix-replatform/lib/progress-log.sh +140 -0
  139. package/skills/wix-replatform/lib/url-preservation-state.js +391 -0
  140. package/skills/wix-replatform/resources/rp-discovery/SKILL.md +184 -0
  141. package/skills/wix-replatform/resources/rp-execute-import/SKILL.md +529 -0
  142. package/skills/wix-replatform/resources/rp-execute-setup/SKILL.md +316 -0
  143. package/skills/wix-replatform/resources/rp-execute-setup/scripts/mint-token.sh +64 -0
  144. package/skills/wix-replatform/resources/rp-import-codegen/SKILL.md +1114 -0
  145. package/skills/wix-replatform/resources/rp-mapper/SKILL.md +430 -0
  146. package/skills/wix-replatform/resources/rp-mapper/lib/mapping-resolve.js +217 -0
  147. package/skills/wix-replatform/resources/rp-mapper/scripts/resolve-mapping.js +148 -0
  148. package/skills/wix-replatform/resources/rp-mapper/wix-business-solutions-taxonomy.md +378 -0
  149. package/skills/wix-replatform/resources/rp-setup-discovery/SKILL.md +183 -0
  150. package/skills/wix-replatform/resources/rp-source-csv/SKILL.md +478 -0
  151. package/skills/wix-replatform/resources/rp-source-csv/lib/canonical-record.js +136 -0
  152. package/skills/wix-replatform/resources/rp-source-csv/lib/csv-fileset.js +0 -0
  153. package/skills/wix-replatform/resources/rp-source-csv/lib/csv-fingerprint.js +310 -0
  154. package/skills/wix-replatform/resources/rp-source-csv/lib/csv-layout.js +1081 -0
  155. package/skills/wix-replatform/resources/rp-source-csv/lib/csv-parse.js +618 -0
  156. package/skills/wix-replatform/resources/rp-source-csv/scripts/csv-discovery.js +949 -0
  157. package/skills/wix-replatform/resources/rp-source-csv/source-schema.example.json +196 -0
  158. package/skills/wix-replatform/resources/rp-source-csv/vendors/bigcommerce.json +66 -0
  159. package/skills/wix-replatform/resources/rp-source-csv/vendors/magento.json +60 -0
  160. package/skills/wix-replatform/resources/rp-source-csv/vendors/shopify.json +95 -0
  161. package/skills/wix-replatform/resources/rp-source-csv/vendors/woocommerce.json +82 -0
  162. package/skills/wix-replatform/resources/rp-source-wordpress/SKILL.md +256 -0
  163. package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-http.js +249 -0
  164. package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-route-classifier.js +444 -0
  165. package/skills/wix-replatform/resources/rp-source-wordpress/scripts/wp-discovery.js +774 -0
  166. package/skills/wix-replatform/resources/rp-source-wordpress/source-schema.example.json +71 -0
  167. package/skills/wix-replatform/resources/rp-target-wix/SKILL.md +632 -0
  168. package/skills/wix-replatform/resources/rp-target-wix/SPEC-product-reviews-and-blog-authorship.md +330 -0
  169. package/skills/wix-replatform/resources/rp-target-wix/domains/README.md +21 -0
  170. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/domain.json +19 -0
  171. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/category.json +37 -0
  172. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/comment.json +37 -0
  173. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/post.json +39 -0
  174. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/tag.json +35 -0
  175. package/skills/wix-replatform/resources/rp-target-wix/domains/cms/domain.json +14 -0
  176. package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/collection.json +35 -0
  177. package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/data-item.json +36 -0
  178. package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/reference.json +35 -0
  179. package/skills/wix-replatform/resources/rp-target-wix/domains/crm/domain.json +14 -0
  180. package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/contact.json +131 -0
  181. package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/email-subscription.json +37 -0
  182. package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/label.json +37 -0
  183. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/domain.json +21 -0
  184. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-line-item.json +37 -0
  185. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-refund.json +37 -0
  186. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order.json +97 -0
  187. package/skills/wix-replatform/resources/rp-target-wix/domains/index.json +528 -0
  188. package/skills/wix-replatform/resources/rp-target-wix/domains/media/domain.json +14 -0
  189. package/skills/wix-replatform/resources/rp-target-wix/domains/media/entities/media-file.json +38 -0
  190. package/skills/wix-replatform/resources/rp-target-wix/domains/media/entities/media-folder.json +37 -0
  191. package/skills/wix-replatform/resources/rp-target-wix/domains/members/domain.json +13 -0
  192. package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/badge.json +37 -0
  193. package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/member.json +53 -0
  194. package/skills/wix-replatform/resources/rp-target-wix/domains/schema.json +84 -0
  195. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/domain.json +25 -0
  196. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/category.json +37 -0
  197. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/coupon.json +37 -0
  198. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/inventory-item.json +38 -0
  199. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product-tag.json +37 -0
  200. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product.json +90 -0
  201. package/skills/wix-replatform/resources/rp-target-wix/lib/audit-summary.js +64 -0
  202. package/skills/wix-replatform/resources/rp-target-wix/lib/contract-ledger.js +141 -0
  203. package/skills/wix-replatform/resources/rp-target-wix/lib/domain-knowledge.js +384 -0
  204. package/skills/wix-replatform/resources/rp-target-wix/lib/ndjson.js +260 -0
  205. package/skills/wix-replatform/resources/rp-target-wix/lib/stores-verification.js +238 -0
  206. package/skills/wix-replatform/resources/rp-target-wix/lib/wix-build.js +237 -0
  207. package/skills/wix-replatform/resources/rp-target-wix/lib/wix-target-spec.js +238 -0
  208. package/skills/wix-replatform/resources/rp-target-wix/lib/wix-writers.js +2219 -0
  209. package/skills/wix-replatform/resources/rp-target-wix/scripts/domain-knowledge-validate.js +31 -0
  210. package/skills/wix-replatform/resources/rp-target-wix/scripts/domain-knowledge.js +100 -0
  211. package/skills/wix-replatform/resources/rp-target-wix/scripts/verify-stores.js +107 -0
  212. package/skills/wix-replatform/resources/rp-telemetry/SKILL.md +344 -0
  213. package/skills/wix-replatform/resources/rp-telemetry/lib/bi-sink.js +250 -0
  214. package/skills/wix-replatform/resources/rp-telemetry/lib/telemetry-recorder.js +1927 -0
  215. package/skills/wix-replatform/resources/rp-telemetry/scripts/meter-from-audit.js +83 -0
  216. package/skills/wix-replatform/resources/rp-telemetry/scripts/rp-telemetry.js +141 -0
  217. package/skills/wix-replatform/scripts/artifact-freshness.js +76 -0
  218. package/skills/wix-replatform/scripts/execution-manifest-validate.js +43 -0
  219. package/skills/wix-replatform/scripts/execution-state-prepare.js +128 -0
  220. package/skills/wix-replatform/scripts/orchestration-preflight.js +48 -0
  221. package/skills/wix-replatform/scripts/orchestration-route.js +46 -0
  222. package/skills/wix-replatform/scripts/orchestration-state.js +245 -0
  223. package/skills/wix-vibe-headless/SKILL.md +54 -25
  224. package/skills/wix-vibe-headless/install/deploy.cjs +95 -0
  225. package/skills/wix-vibe-headless/install/pin-agents-md.cjs +34 -0
  226. package/skills/wix-vibe-headless/platforms/README.md +26 -0
  227. package/skills/wix-vibe-headless/platforms/base44.md +177 -0
  228. package/skills/wix-vibe-headless/platforms/generic.md +122 -0
  229. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +230 -119
  230. package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +43 -0
  231. package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +25 -0
  232. package/skills/wix-vibe-headless/references/blog/app/components/PostGrid.jsx +15 -0
  233. package/skills/wix-vibe-headless/references/blog/app/context/TaxonomyContext.jsx +35 -0
  234. package/skills/wix-vibe-headless/references/blog/app/hooks/usePostDetail.js +38 -0
  235. package/skills/wix-vibe-headless/references/blog/app/pages/Blog.jsx +40 -0
  236. package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +54 -0
  237. package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +65 -0
  238. package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +50 -0
  239. package/skills/wix-vibe-headless/references/blog/seed/SEED.md +103 -0
  240. package/skills/wix-vibe-headless/references/blog/seed/seed-blog.js +270 -0
  241. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +260 -145
  242. package/skills/wix-vibe-headless/references/bookings/app/components/BookingForm.jsx +65 -0
  243. package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +47 -0
  244. package/skills/wix-vibe-headless/references/bookings/app/components/ServiceGrid.jsx +13 -0
  245. package/skills/wix-vibe-headless/references/bookings/app/components/SlotPicker.jsx +126 -0
  246. package/skills/wix-vibe-headless/references/bookings/app/hooks/useServiceDetail.js +108 -0
  247. package/skills/wix-vibe-headless/references/bookings/app/hooks/useServices.js +58 -0
  248. package/skills/wix-vibe-headless/references/bookings/app/lib/serviceFacts.js +95 -0
  249. package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +94 -0
  250. package/skills/wix-vibe-headless/references/bookings/app/pages/Services.jsx +89 -0
  251. package/skills/wix-vibe-headless/references/bookings/{wix-bookings-checkout.js → app/rest/wix-bookings-checkout.js} +1 -1
  252. package/skills/wix-vibe-headless/references/bookings/{wix-bookings-services.js → app/rest/wix-bookings-services.js} +90 -12
  253. package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +117 -0
  254. package/skills/wix-vibe-headless/references/bookings/seed/seed-bookings.js +333 -0
  255. package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +169 -147
  256. package/skills/wix-vibe-headless/references/cms/app/collection.config.js +31 -0
  257. package/skills/wix-vibe-headless/references/cms/app/lib/wixImage.js +18 -0
  258. package/skills/wix-vibe-headless/references/cms/{wix-cms.js → app/rest/wix-cms.js} +47 -2
  259. package/skills/wix-vibe-headless/references/cms/seed/SEED.md +145 -0
  260. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +228 -131
  261. package/skills/wix-vibe-headless/references/events/app/components/CategoryFilter.jsx +23 -0
  262. package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +43 -0
  263. package/skills/wix-vibe-headless/references/events/app/components/EventGrid.jsx +13 -0
  264. package/skills/wix-vibe-headless/references/events/app/components/EventRegistration.jsx +29 -0
  265. package/skills/wix-vibe-headless/references/events/app/components/RsvpForm.jsx +57 -0
  266. package/skills/wix-vibe-headless/references/events/app/components/TicketPicker.jsx +78 -0
  267. package/skills/wix-vibe-headless/references/events/app/hooks/useEventDetail.js +27 -0
  268. package/skills/wix-vibe-headless/references/events/app/hooks/useEventsList.js +54 -0
  269. package/skills/wix-vibe-headless/references/events/app/hooks/useRsvpForm.js +40 -0
  270. package/skills/wix-vibe-headless/references/events/app/hooks/useTicketing.js +81 -0
  271. package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +48 -0
  272. package/skills/wix-vibe-headless/references/events/app/pages/Events.jsx +36 -0
  273. package/skills/wix-vibe-headless/references/events/{wix-events-browse.js → app/rest/wix-events-browse.js} +3 -2
  274. package/skills/wix-vibe-headless/references/events/seed/SEED.md +107 -0
  275. package/skills/wix-vibe-headless/references/events/seed/seed-events.js +251 -0
  276. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +276 -188
  277. package/skills/wix-vibe-headless/references/members/app/components/LoginForm.jsx +101 -0
  278. package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +33 -0
  279. package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +21 -0
  280. package/skills/wix-vibe-headless/references/members/app/components/SocialButtons.jsx +29 -0
  281. package/skills/wix-vibe-headless/references/members/app/context/MemberContext.jsx +54 -0
  282. package/skills/wix-vibe-headless/references/members/app/hooks/useLoginForm.js +77 -0
  283. package/skills/wix-vibe-headless/references/members/app/pages/Account.jsx +36 -0
  284. package/skills/wix-vibe-headless/references/members/app/pages/Callback.jsx +32 -0
  285. package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +35 -0
  286. package/skills/wix-vibe-headless/references/members/{wix-members-auth.js → app/rest/wix-members-auth.js} +6 -3
  287. package/skills/wix-vibe-headless/references/members/install/Register.jsx +22 -0
  288. package/skills/wix-vibe-headless/references/members/seed/SEED.md +22 -0
  289. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +233 -104
  290. package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +31 -0
  291. package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionGrid.jsx +15 -0
  292. package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +39 -0
  293. package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectGrid.jsx +15 -0
  294. package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectMedia.jsx +29 -0
  295. package/skills/wix-vibe-headless/references/portfolio/app/hooks/useCollectionProjects.js +35 -0
  296. package/skills/wix-vibe-headless/references/portfolio/app/hooks/usePortfolioGallery.js +27 -0
  297. package/skills/wix-vibe-headless/references/portfolio/app/hooks/useProjectDetail.js +25 -0
  298. package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +39 -0
  299. package/skills/wix-vibe-headless/references/portfolio/app/pages/Portfolio.jsx +27 -0
  300. package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +60 -0
  301. package/skills/wix-vibe-headless/references/portfolio/{wix-portfolio.js → app/rest/wix-portfolio.js} +84 -55
  302. package/skills/wix-vibe-headless/references/portfolio/seed/SEED.md +94 -0
  303. package/skills/wix-vibe-headless/references/portfolio/seed/seed-portfolio.js +236 -0
  304. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +240 -117
  305. package/skills/wix-vibe-headless/references/pricing-plans/app/components/OrderCard.jsx +35 -0
  306. package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +48 -0
  307. package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanGrid.jsx +16 -0
  308. package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanPrice.jsx +35 -0
  309. package/skills/wix-vibe-headless/references/pricing-plans/app/hooks/useMyPlans.js +25 -0
  310. package/skills/wix-vibe-headless/references/pricing-plans/app/hooks/usePlanDetail.js +28 -0
  311. package/skills/wix-vibe-headless/references/pricing-plans/app/hooks/usePlans.js +30 -0
  312. package/skills/wix-vibe-headless/references/pricing-plans/app/pages/MyPlans.jsx +30 -0
  313. package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +53 -0
  314. package/skills/wix-vibe-headless/references/pricing-plans/app/pages/Plans.jsx +28 -0
  315. package/skills/wix-vibe-headless/references/pricing-plans/seed/SEED.md +86 -0
  316. package/skills/wix-vibe-headless/references/pricing-plans/seed/seed-pricing-plans.js +251 -0
  317. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +248 -124
  318. package/skills/wix-vibe-headless/references/restaurants/app/components/ItemDialog.jsx +81 -0
  319. package/skills/wix-vibe-headless/references/restaurants/app/components/MenuItemCard.jsx +71 -0
  320. package/skills/wix-vibe-headless/references/restaurants/app/components/MenuList.jsx +43 -0
  321. package/skills/wix-vibe-headless/references/restaurants/app/components/OrderCartButton.jsx +21 -0
  322. package/skills/wix-vibe-headless/references/restaurants/app/components/OrderCartDrawer.jsx +60 -0
  323. package/skills/wix-vibe-headless/references/restaurants/app/context/OrderCartContext.jsx +63 -0
  324. package/skills/wix-vibe-headless/references/restaurants/app/hooks/useItemOrder.js +32 -0
  325. package/skills/wix-vibe-headless/references/restaurants/app/hooks/useReservation.js +73 -0
  326. package/skills/wix-vibe-headless/references/restaurants/app/pages/Menu.jsx +28 -0
  327. package/skills/wix-vibe-headless/references/restaurants/app/pages/Reservations.jsx +102 -0
  328. package/skills/wix-vibe-headless/references/restaurants/seed/SEED.md +180 -0
  329. package/skills/wix-vibe-headless/references/restaurants/seed/seed-restaurants.js +452 -0
  330. package/skills/wix-vibe-headless/references/shared/app/components/WixManageBanner.jsx +56 -0
  331. package/skills/wix-vibe-headless/references/shared/{wix-client.js → app/rest/wix-client.js} +6 -8
  332. package/skills/wix-vibe-headless/references/shared/app/rest/wix-config.js +6 -0
  333. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +263 -148
  334. package/skills/wix-vibe-headless/references/storefront/app/components/CartButton.jsx +20 -0
  335. package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +150 -0
  336. package/skills/wix-vibe-headless/references/storefront/app/components/ProductCard.jsx +167 -0
  337. package/skills/wix-vibe-headless/references/storefront/app/components/ProductGallery.jsx +51 -0
  338. package/skills/wix-vibe-headless/references/storefront/app/components/ProductGrid.jsx +54 -0
  339. package/skills/wix-vibe-headless/references/storefront/app/components/VariantPicker.jsx +119 -0
  340. package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +71 -0
  341. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +109 -0
  342. package/skills/wix-vibe-headless/references/storefront/app/hooks/useShop.js +98 -0
  343. package/skills/wix-vibe-headless/references/storefront/app/lib/storeImage.js +27 -0
  344. package/skills/wix-vibe-headless/references/storefront/app/pages/ProductDetail.jsx +139 -0
  345. package/skills/wix-vibe-headless/references/storefront/app/pages/Shop.jsx +75 -0
  346. package/skills/wix-vibe-headless/references/storefront/{wix-store-cart.js → app/rest/wix-store-cart.js} +1 -1
  347. package/skills/wix-vibe-headless/references/storefront/{wix-store-catalog.js → app/rest/wix-store-catalog.js} +25 -2
  348. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +111 -0
  349. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.js +299 -0
  350. /package/skills/wix-vibe-headless/references/blog/{wix-blog.js → app/rest/wix-blog.js} +0 -0
  351. /package/skills/wix-vibe-headless/references/events/{wix-events-registration.js → app/rest/wix-events-registration.js} +0 -0
  352. /package/skills/wix-vibe-headless/references/pricing-plans/{wix-pricing-plans.js → app/rest/wix-pricing-plans.js} +0 -0
  353. /package/skills/wix-vibe-headless/references/restaurants/{wix-restaurants-menu.js → app/rest/wix-restaurants-menu.js} +0 -0
  354. /package/skills/wix-vibe-headless/references/restaurants/{wix-restaurants-ordering.js → app/rest/wix-restaurants-ordering.js} +0 -0
  355. /package/skills/wix-vibe-headless/references/restaurants/{wix-restaurants-reservations.js → app/rest/wix-restaurants-reservations.js} +0 -0
@@ -0,0 +1,1927 @@
1
+ 'use strict';
2
+
3
+ // Telemetry recorder for RePlatform migration runs.
4
+ //
5
+ // The recorder owns everything mechanical and error-prone: schema validation
6
+ // (enums, subtype-per-event-type), timestamping, per-class event aggregation,
7
+ // the run/attempt/session resume model, the mechanical privacy scrub, atomic
8
+ // journal appends, and the well-formedness gate at finalize. The rp-telemetry
9
+ // companion skill carries judgment only (what to record, which subtype,
10
+ // observation-only prose) and always persists through this module — the signal
11
+ // layer is never hand-written.
12
+ //
13
+ // Storage layout, per migration project:
14
+ // telemetry/events.jsonl append-only journal of the
15
+ // current (unfinalized) run
16
+ // run-telemetry.json latest finalized signal doc
17
+ // telemetry/runs/run-<attempt>-<run_id>.json archived finalized signal docs
18
+ // telemetry/runs/run-<attempt>-<run_id>.jsonl archived raw journals
19
+ //
20
+ // The journal is the source of truth during a run; the finalized document is a
21
+ // pure function of the journal (assembleDocument), so a crash between the
22
+ // finalize append and the document write is recoverable on the next call.
23
+
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+ const os = require('node:os');
27
+ const crypto = require('node:crypto');
28
+ const { execFileSync } = require('node:child_process');
29
+ const biSink = require('./bi-sink.js');
30
+
31
+ // 1.1.0 — added `skills_commit` to the run-start record and rollup (additive).
32
+ // 1.2.0 — BI sink (spec 0007): added `telemetry_health.bi_push_failures`, the
33
+ // `bi_push_failed`/`bi_push_truncated` journal record types (additive;
34
+ // readers per the external reader contract skip unknown record types).
35
+ const TELEMETRY_SCHEMA_VERSION = '1.2.0';
36
+ const FREE_TEXT_MAX = 400;
37
+ const SHAPES_KEPT_PER_CLASS = 3;
38
+ const SHAPES_MAX_PER_CALL = 10;
39
+ const EVIDENCE_REFS_MAX = 5;
40
+ // A resume gap below this is treated as in-session noise, not a wait interval.
41
+ const IMPLICIT_WAIT_MIN_MS = 60 * 1000;
42
+ // A wait at least this long with no halt_needs_user event in its stage means the
43
+ // capture lost *why* the run stalled — flagged at finalize, never silent.
44
+ const WAIT_WITHOUT_HALT_FLAG_MS = 5 * 60 * 1000;
45
+ // Metered durations may legitimately overlap slightly with elapsed time (clock granularity,
46
+ // concurrent API calls inside one stage), so allow modest slack before calling it a contradiction.
47
+ const OVER_ATTRIBUTION_TOLERANCE = 1.25;
48
+ // Absolute slack on top of the ratio, so a very short stage is not flagged over a few hundred ms.
49
+ const OVER_ATTRIBUTION_FLOOR_MS = 5 * 1000;
50
+ const EVIDENCE_SIZE_FLAG_BYTES = 2 * 1024 * 1024;
51
+
52
+ const STAGES = [
53
+ 'config', 'discovery', 'mcp_gate', 'mapping', 'mapping_review', 'setup_discovery',
54
+ 'codegen', 'approval_gate', 'setup_provisioning', 'storefront_build', 'extract',
55
+ 'import', 'finish',
56
+ ];
57
+ const STAGE_OUTCOMES = ['passed', 'halted', 'failed', 'skipped'];
58
+ const TERMINAL_STATES = ['completed', 'halted_needs_user', 'failed', 'abandoned_by_user'];
59
+ const SEVERITIES = ['blocking', 'degraded', 'cosmetic', 'info'];
60
+ const SOURCE_PLATFORMS = ['wordpress', 'woocommerce', 'shopify', 'csv', 'other'];
61
+ const DELIVERY_MODES = ['management', 'website'];
62
+ const DESTINATION_STRATEGIES = ['new_site', 'existing_site'];
63
+ const OPERATOR_ACCEPTANCE = ['accepted', 'rework_needed', 'rejected', 'unknown'];
64
+ const VOLUME_TARGETS = ['native', 'cms', 'none'];
65
+ const VERIFICATION_METHODS = ['query_back', 'route_check', 'manual_inspection'];
66
+
67
+ const EVENT_SUBTYPES = {
68
+ halt_needs_user: ['missing_input', 'manual_only', 'systemic_failure'],
69
+ manual_action_required: ['plan_or_billing', 'dashboard_only', 'external_dependency', 'other'],
70
+ error: null,
71
+ fidelity_loss: ['dropped_field', 'unverified_enum', 'no_target', 'coerced_value'],
72
+ api_gap: ['missing_api', 'missing_capability', 'internal_only', 'other'],
73
+ skill_coverage_gap: ['guessed_value', 'undocumented_workaround', 'ambiguous_instruction', 'path_not_covered'],
74
+ user_decision: ['accepted', 'declined', 'deferred', 'amended'],
75
+ pipeline_defect: ['state_inconsistency', 'ordering_violation', 'record_defect', 'other'],
76
+ };
77
+ const EVENT_TYPES = Object.keys(EVENT_SUBTYPES);
78
+
79
+ // Privacy tiers travel with the record so no sink can ignore them. Floors, not
80
+ // defaults: identifying:* fields are never transmitted raw without the
81
+ // pseudonymization floor or the locus-appropriate authority.
82
+ const FIELD_TIERS = {
83
+ 'rollup.source_site_url': 'identifying:client',
84
+ 'rollup.wix_user_id': 'identifying:operator',
85
+ '*': 'behavioral',
86
+ };
87
+
88
+ const OUTCOME_FOR_TERMINAL = {
89
+ completed: 'passed',
90
+ failed: 'failed',
91
+ halted_needs_user: 'halted',
92
+ abandoned_by_user: 'halted',
93
+ };
94
+
95
+ // Last-line-of-defense scrub over free text, shape field names, and locators.
96
+ // Order matters: broader assignment/JWT shapes before generic opaque strings.
97
+ const SCRUB_PATTERNS = [
98
+ ['secret_assignment', /\b(?:token|secret|password|passwd|api[_-]?key|authorization|bearer)\b\s*[:=]\s*\S+/gi],
99
+ ['jwt', /\beyJ[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]{4,}){1,4}\b/g],
100
+ ['url', /\b(?:https?|ftp):\/\/\S+/gi],
101
+ ['email', /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g],
102
+ ['opaque', /(?=[A-Za-z0-9+/_-]*[0-9])[A-Za-z0-9+/_-]{32,}={0,2}/g],
103
+ ];
104
+
105
+ class ValidationError extends Error {
106
+ constructor(errors, hint) {
107
+ super(Array.isArray(errors) ? errors.join('; ') : String(errors));
108
+ this.name = 'ValidationError';
109
+ this.errors = Array.isArray(errors) ? errors : [String(errors)];
110
+ this.hint = hint || null;
111
+ }
112
+ }
113
+
114
+ function nowIso(now) {
115
+ if (now instanceof Date) return now.toISOString();
116
+ if (typeof now === 'string') return new Date(now).toISOString();
117
+ return new Date().toISOString();
118
+ }
119
+
120
+ function tsMs(iso) {
121
+ return Date.parse(iso);
122
+ }
123
+
124
+ function telemetryDir(projectDir) {
125
+ return path.join(projectDir, 'telemetry');
126
+ }
127
+
128
+ function journalPath(projectDir) {
129
+ return path.join(telemetryDir(projectDir), 'events.jsonl');
130
+ }
131
+
132
+ function runsDir(projectDir) {
133
+ return path.join(telemetryDir(projectDir), 'runs');
134
+ }
135
+
136
+ function signalPath(projectDir) {
137
+ return path.join(projectDir, 'run-telemetry.json');
138
+ }
139
+
140
+ function scrubText(value) {
141
+ let hits = 0;
142
+ let text = String(value);
143
+ for (const [name, re] of SCRUB_PATTERNS) {
144
+ text = text.replace(re, () => {
145
+ hits += 1;
146
+ return `[scrubbed:${name}]`;
147
+ });
148
+ }
149
+ return { text, hits };
150
+ }
151
+
152
+ function normalizeToken(value) {
153
+ return String(value)
154
+ .toLowerCase()
155
+ .trim()
156
+ .replace(/[^a-z0-9]+/g, '_')
157
+ .replace(/^_+|_+$/g, '');
158
+ }
159
+
160
+ function stableStringify(value) {
161
+ if (Array.isArray(value)) {
162
+ return `[${value.map(stableStringify).join(',')}]`;
163
+ }
164
+ if (value && typeof value === 'object') {
165
+ const keys = Object.keys(value).sort();
166
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
167
+ }
168
+ return JSON.stringify(value);
169
+ }
170
+
171
+ function checkEnum(errors, field, value, allowed) {
172
+ if (!allowed.includes(value)) {
173
+ errors.push(`${field} must be one of: ${allowed.join(', ')} (got: ${JSON.stringify(value)})`);
174
+ return false;
175
+ }
176
+ return true;
177
+ }
178
+
179
+ function checkBoundedText(errors, field, value, { required = false, max = FREE_TEXT_MAX } = {}) {
180
+ if (value === undefined || value === null) {
181
+ if (required) errors.push(`${field} is required (a sentence or two, observation-only)`);
182
+ return null;
183
+ }
184
+ if (typeof value !== 'string' || value.trim() === '') {
185
+ errors.push(`${field} must be a non-empty string`);
186
+ return null;
187
+ }
188
+ if (value.length > max) {
189
+ errors.push(`${field} exceeds ${max} chars (got ${value.length}) — keep it to a sentence or two; the coded fields carry the structure`);
190
+ return null;
191
+ }
192
+ return value.trim();
193
+ }
194
+
195
+ function checkApiSurface(errors, field, value) {
196
+ if (value === undefined || value === null) return null;
197
+ const str = String(value);
198
+ if (/:\/\//.test(str) || /[?\s]/.test(str)) {
199
+ errors.push(`${field} must be an API/endpoint class like "stores/v3" or "wp/v2/posts", never a URL (URLs can embed auth)`);
200
+ return null;
201
+ }
202
+ if (str.length > 64 || !/^[A-Za-z0-9/_.:-]+$/.test(str)) {
203
+ errors.push(`${field} must be a short endpoint class matching [A-Za-z0-9/_.:-], max 64 chars`);
204
+ return null;
205
+ }
206
+ return str;
207
+ }
208
+
209
+ function checkOpaqueId(errors, field, value, { max = 64 } = {}) {
210
+ if (value === undefined || value === null) return null;
211
+ const str = String(value);
212
+ if (/[\s]/.test(str) || /:\/\//.test(str) || str.length > max) {
213
+ errors.push(`${field} must be an opaque identifier (no whitespace, no URL), max ${max} chars`);
214
+ return null;
215
+ }
216
+ return str;
217
+ }
218
+
219
+ function checkCount(errors, field, value, { defaultValue = 0, min = 0 } = {}) {
220
+ if (value === undefined || value === null) return defaultValue;
221
+ if (!Number.isInteger(value) || value < min) {
222
+ errors.push(`${field} must be an integer >= ${min}`);
223
+ return defaultValue;
224
+ }
225
+ return value;
226
+ }
227
+
228
+ function normalizeOrigin(errors, value) {
229
+ try {
230
+ const url = new URL(String(value));
231
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
232
+ errors.push('source_site_url must be an http(s) URL');
233
+ return null;
234
+ }
235
+ // Bare origin only — drops path, query, and any embedded credentials.
236
+ return `${url.protocol}//${url.host}`;
237
+ } catch {
238
+ errors.push(`source_site_url is not a parseable URL: ${JSON.stringify(String(value))}`);
239
+ return null;
240
+ }
241
+ }
242
+
243
+ function sanitizeShapeValue(value, errors, keyPath, depth, counter) {
244
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') {
245
+ return value;
246
+ }
247
+ if (typeof value === 'string') {
248
+ if (value.length > 64) {
249
+ errors.push(`observed_shapes${keyPath} string value exceeds 64 chars — shapes carry field names and types, never record values`);
250
+ return null;
251
+ }
252
+ const { text, hits } = scrubText(value);
253
+ counter.hits += hits;
254
+ return text;
255
+ }
256
+ if (depth >= 4) {
257
+ errors.push(`observed_shapes${keyPath} nests deeper than 4 levels`);
258
+ return null;
259
+ }
260
+ if (Array.isArray(value)) {
261
+ return value.map((item, i) => sanitizeShapeValue(item, errors, `${keyPath}[${i}]`, depth + 1, counter));
262
+ }
263
+ if (typeof value === 'object') {
264
+ const out = {};
265
+ for (const [key, item] of Object.entries(value)) {
266
+ // Custom field names on a client site can themselves be identifying.
267
+ const { text, hits } = scrubText(key);
268
+ counter.hits += hits;
269
+ out[text] = sanitizeShapeValue(item, errors, `${keyPath}.${key}`, depth + 1, counter);
270
+ }
271
+ return out;
272
+ }
273
+ errors.push(`observed_shapes${keyPath} has unsupported value type ${typeof value}`);
274
+ return null;
275
+ }
276
+
277
+ function validateEvidenceRefs(errors, value) {
278
+ if (value === undefined || value === null) return null;
279
+ if (!Array.isArray(value) || value.length === 0) {
280
+ errors.push('evidence_refs must be a non-empty array of {artifact, locator?} or omitted');
281
+ return null;
282
+ }
283
+ if (value.length > EVIDENCE_REFS_MAX) {
284
+ errors.push(`evidence_refs is capped at ${EVIDENCE_REFS_MAX} sample refs per event`);
285
+ return null;
286
+ }
287
+ const refs = [];
288
+ for (const [i, ref] of value.entries()) {
289
+ if (!ref || typeof ref !== 'object' || typeof ref.artifact !== 'string') {
290
+ errors.push(`evidence_refs[${i}] must be {artifact: <project-relative path>, locator?: <heading/anchor>}`);
291
+ continue;
292
+ }
293
+ const artifact = ref.artifact.replace(/\\/g, '/');
294
+ if (path.isAbsolute(artifact) || artifact.split('/').includes('..')) {
295
+ errors.push(`evidence_refs[${i}].artifact must be a project-root-relative path (no absolute paths, no ..)`);
296
+ continue;
297
+ }
298
+ if (/(^|\/)config\//.test(artifact) || artifact.endsWith('.env')) {
299
+ errors.push(`evidence_refs[${i}].artifact must never point at secret-bearing config files`);
300
+ continue;
301
+ }
302
+ let locator = null;
303
+ if (ref.locator !== undefined && ref.locator !== null) {
304
+ if (typeof ref.locator !== 'string' || ref.locator.length > 120) {
305
+ errors.push(`evidence_refs[${i}].locator must be a string of at most 120 chars (prefer a section heading — line ranges break on regeneration)`);
306
+ continue;
307
+ }
308
+ locator = scrubText(ref.locator).text;
309
+ }
310
+ refs.push({ artifact, locator });
311
+ }
312
+ return refs;
313
+ }
314
+
315
+ // --- dimension (rollup identity) fields -------------------------------------
316
+
317
+ // --- cost/latency metering -------------------------------------------------
318
+ // Duration fields split `active_ms` into its three real components. Before this existed,
319
+ // active_ms was a single wall-clock number fusing agent reasoning, subprocess execution, remote
320
+ // API latency and defect-repair time — which made it impossible to localize a bottleneck.
321
+ //
322
+ // Anything NOT attributed by a meter call stays visible as `unattributed_ms`. That is deliberate:
323
+ // a partially-metered stage must not look fully explained.
324
+ const METER_DURATION_KEYS = ['model_ms', 'api_ms', 'script_ms'];
325
+ const METER_COUNT_KEYS = ['input_tokens', 'output_tokens', 'cache_read_tokens', 'cache_write_tokens', 'api_calls', 'api_retries'];
326
+ const METER_ALLOWED_KEYS = new Set([...METER_DURATION_KEYS, ...METER_COUNT_KEYS, 'stage']);
327
+
328
+ function validateMeter(input, { openStage, lastStage }) {
329
+ const errors = [];
330
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
331
+ throw new ValidationError(['meter payload must be a JSON object']);
332
+ }
333
+ for (const key of Object.keys(input)) {
334
+ if (!METER_ALLOWED_KEYS.has(key)) {
335
+ errors.push(`unknown meter field: ${key} (allowed: ${[...METER_ALLOWED_KEYS].join(', ')})`);
336
+ }
337
+ }
338
+ const stageName = input.stage || openStage || lastStage;
339
+ if (!stageName) errors.push('no stage is open and none was named; pass --stage <stage>');
340
+ else checkEnum(errors, 'stage', stageName, STAGES);
341
+
342
+ const out = { stage: stageName };
343
+ let any = false;
344
+ for (const key of [...METER_DURATION_KEYS, ...METER_COUNT_KEYS]) {
345
+ if (input[key] === undefined || input[key] === null) continue;
346
+ const value = input[key];
347
+ if (!Number.isFinite(value) || value < 0 || Math.floor(value) !== value) {
348
+ errors.push(`${key} must be a non-negative integer`);
349
+ continue;
350
+ }
351
+ out[key] = value;
352
+ any = true;
353
+ }
354
+ if (!any) errors.push(`meter needs at least one measurement (${[...METER_DURATION_KEYS, ...METER_COUNT_KEYS].join(', ')})`);
355
+ if (errors.length > 0) {
356
+ throw new ValidationError(errors, 'meter records measured numbers only — never estimate them by hand');
357
+ }
358
+ return out;
359
+ }
360
+
361
+ // A pricing snapshot makes historical cost recomputable when list prices change. Rates are per
362
+ // MILLION tokens, matching how model pricing is published.
363
+ function validatePricingSnapshot(errors, value) {
364
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
365
+ errors.push('model_pricing_snapshot must be an object of { <model>: { input_per_mtok, output_per_mtok, ... } }');
366
+ return undefined;
367
+ }
368
+ const out = {};
369
+ const rateKeys = ['input_per_mtok', 'output_per_mtok', 'cache_read_per_mtok', 'cache_write_per_mtok'];
370
+ for (const [model, rates] of Object.entries(value)) {
371
+ if (typeof model !== 'string' || model.length > 64) {
372
+ errors.push('model_pricing_snapshot keys must be short model ids');
373
+ continue;
374
+ }
375
+ if (!rates || typeof rates !== 'object' || Array.isArray(rates)) {
376
+ errors.push(`model_pricing_snapshot.${model} must be an object of numeric rates`);
377
+ continue;
378
+ }
379
+ const entry = {};
380
+ for (const [key, rate] of Object.entries(rates)) {
381
+ if (!rateKeys.includes(key)) {
382
+ errors.push(`model_pricing_snapshot.${model}.${key} is not a known rate (allowed: ${rateKeys.join(', ')})`);
383
+ continue;
384
+ }
385
+ if (!Number.isFinite(rate) || rate < 0) {
386
+ errors.push(`model_pricing_snapshot.${model}.${key} must be a non-negative number`);
387
+ continue;
388
+ }
389
+ entry[key] = rate;
390
+ }
391
+ out[model] = entry;
392
+ }
393
+ return out;
394
+ }
395
+
396
+ const DIM_VALIDATORS = {
397
+ model_pricing_snapshot: (errors, v) => validatePricingSnapshot(errors, v),
398
+ source_platform: (errors, v) => (checkEnum(errors, 'source_platform', v, SOURCE_PLATFORMS) ? v : undefined),
399
+ source_platform_version: (errors, v) => {
400
+ if (typeof v !== 'string' || v.length > 32) {
401
+ errors.push('source_platform_version must be a short version string');
402
+ return undefined;
403
+ }
404
+ return v;
405
+ },
406
+ source_site_url: (errors, v) => {
407
+ const origin = normalizeOrigin(errors, v);
408
+ return origin === null ? undefined : origin;
409
+ },
410
+ source_extensions: (errors, v) => {
411
+ if (!Array.isArray(v) || v.length > 64 || v.some((item) => typeof item !== 'string' || item.length > 48)) {
412
+ errors.push('source_extensions must be an array of short extension/plugin class names');
413
+ return undefined;
414
+ }
415
+ return [...new Set(v.map(normalizeToken).filter(Boolean))];
416
+ },
417
+ source_acquisition: (errors, v) => {
418
+ const token = normalizeToken(v);
419
+ if (!token || token.length > 32) {
420
+ errors.push('source_acquisition must be a short class token, e.g. admin_api | public_storefront | file_export');
421
+ return undefined;
422
+ }
423
+ return token;
424
+ },
425
+ delivery_mode: (errors, v) => (checkEnum(errors, 'delivery_mode', v, DELIVERY_MODES) ? v : undefined),
426
+ destination_strategy: (errors, v) => (checkEnum(errors, 'destination_strategy', v, DESTINATION_STRATEGIES) ? v : undefined),
427
+ site_id: (errors, v) => {
428
+ const id = checkOpaqueId(errors, 'site_id', v);
429
+ return id === null ? undefined : id;
430
+ },
431
+ wix_user_id: (errors, v) => {
432
+ const id = checkOpaqueId(errors, 'wix_user_id', v);
433
+ return id === null ? undefined : id;
434
+ },
435
+ skills_version: (errors, v) => {
436
+ if (typeof v !== 'string' || v.length > 32) {
437
+ errors.push('skills_version must be a short version string');
438
+ return undefined;
439
+ }
440
+ return v.trim();
441
+ },
442
+ skills_commit: (errors, v) => {
443
+ if (typeof v !== 'string' || !/^[0-9a-f]{7,40}$/.test(v.trim())) {
444
+ errors.push('skills_commit must be a git commit SHA (7-40 hex chars)');
445
+ return undefined;
446
+ }
447
+ return v.trim();
448
+ },
449
+ runtime_env: (errors, v) => {
450
+ if (!v || typeof v !== 'object' || Array.isArray(v)) {
451
+ errors.push('runtime_env must be an object with {os?, node_version?, agent_runtime?, model?}');
452
+ return undefined;
453
+ }
454
+ const out = {};
455
+ for (const key of ['os', 'node_version', 'agent_runtime', 'model']) {
456
+ if (v[key] === undefined || v[key] === null) continue;
457
+ if (typeof v[key] !== 'string' || v[key].length > 64 || /[/\\]/.test(v[key])) {
458
+ errors.push(`runtime_env.${key} must be a coarse platform fact (short string, no paths)`);
459
+ continue;
460
+ }
461
+ out[key] = v[key];
462
+ }
463
+ const unknown = Object.keys(v).filter((k) => !['os', 'node_version', 'agent_runtime', 'model'].includes(k));
464
+ if (unknown.length > 0) {
465
+ errors.push(`runtime_env has unknown keys: ${unknown.join(', ')}`);
466
+ return undefined;
467
+ }
468
+ return out;
469
+ },
470
+ };
471
+
472
+ function validateDims(input) {
473
+ if (input === undefined || input === null) return {};
474
+ if (typeof input !== 'object' || Array.isArray(input)) {
475
+ throw new ValidationError(['dimensions must be a JSON object']);
476
+ }
477
+ const errors = [];
478
+ const dims = {};
479
+ for (const [key, value] of Object.entries(input)) {
480
+ if (value === undefined || value === null) continue;
481
+ const validator = DIM_VALIDATORS[key];
482
+ if (!validator) {
483
+ errors.push(`unknown dimension field: ${key} (allowed: ${Object.keys(DIM_VALIDATORS).join(', ')})`);
484
+ continue;
485
+ }
486
+ const normalized = validator(errors, value);
487
+ if (normalized !== undefined) dims[key] = normalized;
488
+ }
489
+ if (errors.length > 0) throw new ValidationError(errors);
490
+ return dims;
491
+ }
492
+
493
+ // --- event validation --------------------------------------------------------
494
+
495
+ const EVENT_ALLOWED_KEYS = new Set([
496
+ 'event_type', 'subtype', 'stage', 'skill', 'entity_type', 'severity',
497
+ 'wix_api_surface', 'source_api_surface', 'wix_app_id', 'error_code',
498
+ 'what_happened', 'expected', 'actual', 'observed_shapes', 'evidence_refs',
499
+ 'count', 'retry_count', 'recovered', 'decision_point',
500
+ ]);
501
+
502
+ function validateEvent(input, context) {
503
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
504
+ throw new ValidationError(['event must be a JSON object']);
505
+ }
506
+ const errors = [];
507
+ const scrub = { hits: 0 };
508
+
509
+ for (const key of Object.keys(input)) {
510
+ if (!EVENT_ALLOWED_KEYS.has(key)) {
511
+ errors.push(`unknown event field: ${key} — there is deliberately no fix/root_cause/recommendation field; record the observation only`);
512
+ }
513
+ }
514
+
515
+ const eventType = input.event_type;
516
+ checkEnum(errors, 'event_type', eventType, EVENT_TYPES);
517
+
518
+ let subtype = null;
519
+ if (EVENT_SUBTYPES[eventType] === null) {
520
+ if (input.subtype !== undefined && input.subtype !== null) {
521
+ errors.push(`event_type ${eventType} defines no subtypes (error_code is its discriminator)`);
522
+ }
523
+ } else if (EVENT_SUBTYPES[eventType]) {
524
+ if (checkEnum(errors, `subtype (for ${eventType})`, input.subtype, EVENT_SUBTYPES[eventType])) {
525
+ subtype = input.subtype;
526
+ }
527
+ }
528
+
529
+ let stage = input.stage;
530
+ if (stage === undefined || stage === null) {
531
+ stage = context.openStage || context.lastStage || null;
532
+ if (!stage) {
533
+ errors.push('stage is required (no stage has been started yet to default to)');
534
+ }
535
+ } else {
536
+ checkEnum(errors, 'stage', stage, STAGES);
537
+ }
538
+
539
+ if (typeof input.skill !== 'string' || !/^[a-z][a-z0-9-]*$/.test(input.skill)) {
540
+ errors.push('skill is required — the active rp-* resource (e.g. "rp-mapper") or "replatform"');
541
+ }
542
+
543
+ checkEnum(errors, 'severity', input.severity, SEVERITIES);
544
+
545
+ let entityType = null;
546
+ if (input.entity_type !== undefined && input.entity_type !== null) {
547
+ entityType = normalizeToken(input.entity_type);
548
+ if (!entityType || entityType.length > 32) {
549
+ errors.push('entity_type must be a short entity class token (e.g. product, post, media)');
550
+ entityType = null;
551
+ }
552
+ }
553
+
554
+ const wixApiSurface = checkApiSurface(errors, 'wix_api_surface', input.wix_api_surface);
555
+ const sourceApiSurface = checkApiSurface(errors, 'source_api_surface', input.source_api_surface);
556
+ const wixAppId = checkOpaqueId(errors, 'wix_app_id', input.wix_app_id);
557
+
558
+ let errorCode = null;
559
+ if (input.error_code !== undefined && input.error_code !== null) {
560
+ const str = String(input.error_code);
561
+ if (str.length > 48 || /\s/.test(str)) {
562
+ errors.push('error_code must be a short machine code (e.g. 428, WDE0110)');
563
+ } else {
564
+ errorCode = str;
565
+ }
566
+ }
567
+ if (eventType === 'error' && errorCode === null) {
568
+ errors.push('error events require error_code — it is the discriminator for this type');
569
+ }
570
+ if (eventType === 'api_gap' && wixApiSurface === null) {
571
+ errors.push('api_gap events require wix_api_surface — it is part of the stable signature');
572
+ }
573
+
574
+ const whatHappened = checkBoundedText(errors, 'what_happened', input.what_happened, { required: true });
575
+ const expected = checkBoundedText(errors, 'expected', input.expected);
576
+ const actual = checkBoundedText(errors, 'actual', input.actual);
577
+
578
+ const count = checkCount(errors, 'count', input.count, { defaultValue: 1, min: 1 });
579
+
580
+ let retryCount;
581
+ let recovered;
582
+ if (eventType === 'error') {
583
+ retryCount = checkCount(errors, 'retry_count', input.retry_count, { defaultValue: 0, min: 0 });
584
+ recovered = input.recovered === undefined || input.recovered === null ? false : input.recovered;
585
+ if (typeof recovered !== 'boolean') {
586
+ errors.push('recovered must be a boolean');
587
+ recovered = false;
588
+ }
589
+ } else {
590
+ if (input.retry_count !== undefined || input.recovered !== undefined) {
591
+ errors.push('retry_count/recovered are only valid on error events');
592
+ }
593
+ }
594
+
595
+ let decisionPoint;
596
+ if (eventType === 'user_decision') {
597
+ decisionPoint = typeof input.decision_point === 'string' ? normalizeToken(input.decision_point) : '';
598
+ if (!decisionPoint || decisionPoint.length > 48) {
599
+ errors.push('user_decision events require decision_point — which checkpoint or fork (e.g. mapping_review, approval_gate)');
600
+ }
601
+ } else if (input.decision_point !== undefined) {
602
+ errors.push('decision_point is only valid on user_decision events');
603
+ }
604
+
605
+ let observedShapes = null;
606
+ if (input.observed_shapes !== undefined && input.observed_shapes !== null) {
607
+ if (!Array.isArray(input.observed_shapes) || input.observed_shapes.length === 0) {
608
+ errors.push('observed_shapes must be a non-empty array of sanitized shape objects or omitted');
609
+ } else if (input.observed_shapes.length > SHAPES_MAX_PER_CALL) {
610
+ errors.push(`observed_shapes is capped at ${SHAPES_MAX_PER_CALL} shapes per call (folding keeps ${SHAPES_KEPT_PER_CLASS} distinct per class)`);
611
+ } else {
612
+ observedShapes = input.observed_shapes.map((shape, i) => {
613
+ if (!shape || typeof shape !== 'object' || Array.isArray(shape)) {
614
+ errors.push(`observed_shapes[${i}] must be an object of field names and types`);
615
+ return null;
616
+ }
617
+ return sanitizeShapeValue(shape, errors, `[${i}]`, 0, scrub);
618
+ });
619
+ }
620
+ }
621
+
622
+ const evidenceRefs = validateEvidenceRefs(errors, input.evidence_refs);
623
+
624
+ if (errors.length > 0) {
625
+ throw new ValidationError(errors, classKeyFor({
626
+ event_type: eventType, stage, entity_type: entityType, wix_api_surface: wixApiSurface,
627
+ source_api_surface: sourceApiSurface, wix_app_id: wixAppId, error_code: errorCode,
628
+ subtype, decision_point: decisionPoint,
629
+ }));
630
+ }
631
+
632
+ const scrubbedWhat = scrubText(whatHappened);
633
+ const scrubbedExpected = expected === null ? null : scrubText(expected);
634
+ const scrubbedActual = actual === null ? null : scrubText(actual);
635
+ scrub.hits += scrubbedWhat.hits + (scrubbedExpected ? scrubbedExpected.hits : 0) + (scrubbedActual ? scrubbedActual.hits : 0);
636
+
637
+ const event = {
638
+ event_type: eventType,
639
+ subtype,
640
+ stage,
641
+ skill: input.skill,
642
+ entity_type: entityType,
643
+ severity: input.severity,
644
+ wix_api_surface: wixApiSurface,
645
+ source_api_surface: sourceApiSurface,
646
+ wix_app_id: wixAppId,
647
+ error_code: errorCode,
648
+ what_happened: scrubbedWhat.text,
649
+ expected: scrubbedExpected ? scrubbedExpected.text : null,
650
+ actual: scrubbedActual ? scrubbedActual.text : null,
651
+ observed_shapes: observedShapes,
652
+ evidence_refs: evidenceRefs,
653
+ count,
654
+ };
655
+ if (eventType === 'error') {
656
+ event.retry_count = retryCount;
657
+ event.recovered = recovered;
658
+ }
659
+ if (eventType === 'user_decision') {
660
+ event.decision_point = decisionPoint;
661
+ }
662
+ return { event, classKey: classKeyFor(event), scrubHits: scrub.hits };
663
+ }
664
+
665
+ function classKeyFor(event) {
666
+ const parts = [
667
+ event.event_type, event.stage, event.entity_type, event.wix_api_surface,
668
+ event.source_api_surface, event.wix_app_id, event.error_code, event.subtype,
669
+ ];
670
+ if (event.event_type === 'user_decision') parts.push(event.decision_point);
671
+ return parts.map((p) => (p === null || p === undefined ? '' : String(p))).join('|');
672
+ }
673
+
674
+ // --- rollup (finalize) validation --------------------------------------------
675
+
676
+ function validateVolumes(errors, input) {
677
+ if (input === undefined || input === null) return [];
678
+ if (!Array.isArray(input)) {
679
+ errors.push('volumes must be an array of per-entity-type volume records');
680
+ return [];
681
+ }
682
+ const volumes = [];
683
+ for (const [i, raw] of input.entries()) {
684
+ if (!raw || typeof raw !== 'object') {
685
+ errors.push(`volumes[${i}] must be an object`);
686
+ continue;
687
+ }
688
+ const entityType = typeof raw.entity_type === 'string' ? normalizeToken(raw.entity_type) : '';
689
+ if (!entityType) {
690
+ errors.push(`volumes[${i}].entity_type is required`);
691
+ continue;
692
+ }
693
+ let target = null;
694
+ if (raw.target !== undefined && raw.target !== null) {
695
+ if (checkEnum(errors, `volumes[${i}].target`, raw.target, VOLUME_TARGETS)) target = raw.target;
696
+ }
697
+ const targetSurface = checkApiSurface(errors, `volumes[${i}].target_surface`, raw.target_surface);
698
+ const volume = {
699
+ entity_type: entityType,
700
+ target,
701
+ target_surface: targetSurface,
702
+ discovered: checkCount(errors, `volumes[${i}].discovered`, raw.discovered),
703
+ planned: raw.planned === undefined || raw.planned === null
704
+ ? null
705
+ : checkCount(errors, `volumes[${i}].planned`, raw.planned),
706
+ attempted: checkCount(errors, `volumes[${i}].attempted`, raw.attempted),
707
+ succeeded: checkCount(errors, `volumes[${i}].succeeded`, raw.succeeded),
708
+ failed: checkCount(errors, `volumes[${i}].failed`, raw.failed),
709
+ skipped: checkCount(errors, `volumes[${i}].skipped`, raw.skipped),
710
+ already_imported: checkCount(errors, `volumes[${i}].already_imported`, raw.already_imported),
711
+ };
712
+ if (volume.attempted !== volume.succeeded + volume.failed) {
713
+ errors.push(`volumes[${i}] (${entityType}): attempted must equal succeeded + failed (${volume.attempted} != ${volume.succeeded} + ${volume.failed})`);
714
+ }
715
+ volumes.push(volume);
716
+ }
717
+ return volumes;
718
+ }
719
+
720
+ function validateVerification(errors, input) {
721
+ if (input === undefined || input === null) return [];
722
+ if (!Array.isArray(input)) {
723
+ errors.push('verification must be an array of verification records');
724
+ return [];
725
+ }
726
+ const records = [];
727
+ for (const [i, raw] of input.entries()) {
728
+ if (!raw || typeof raw !== 'object') {
729
+ errors.push(`verification[${i}] must be an object`);
730
+ continue;
731
+ }
732
+ const subject = typeof raw.subject === 'string' ? normalizeToken(raw.subject) : '';
733
+ if (!subject) {
734
+ errors.push(`verification[${i}].subject is required (an entity_type, or "routes")`);
735
+ continue;
736
+ }
737
+ if (!checkEnum(errors, `verification[${i}].method`, raw.method, VERIFICATION_METHODS)) continue;
738
+ const record = {
739
+ subject,
740
+ method: raw.method,
741
+ checked: checkCount(errors, `verification[${i}].checked`, raw.checked),
742
+ passed: checkCount(errors, `verification[${i}].passed`, raw.passed),
743
+ failed: checkCount(errors, `verification[${i}].failed`, raw.failed),
744
+ };
745
+ if (record.passed + record.failed > record.checked) {
746
+ errors.push(`verification[${i}] (${subject}): passed + failed must not exceed checked`);
747
+ }
748
+ records.push(record);
749
+ }
750
+ return records;
751
+ }
752
+
753
+ function validateFinalizeInput(input, context) {
754
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
755
+ throw new ValidationError(['finalize payload must be a JSON object']);
756
+ }
757
+ const errors = [];
758
+ const dimKeys = {};
759
+ const known = new Set(['terminal_state', 'stopped_at_stage', 'operator_acceptance', 'volumes', 'verification']);
760
+ for (const [key, value] of Object.entries(input)) {
761
+ if (known.has(key)) continue;
762
+ if (DIM_VALIDATORS[key]) {
763
+ dimKeys[key] = value;
764
+ continue;
765
+ }
766
+ errors.push(`unknown finalize field: ${key}`);
767
+ }
768
+
769
+ checkEnum(errors, 'terminal_state', input.terminal_state, TERMINAL_STATES);
770
+
771
+ let stoppedAtStage = null;
772
+ if (input.terminal_state === 'completed') {
773
+ if (input.stopped_at_stage !== undefined && input.stopped_at_stage !== null) {
774
+ errors.push('stopped_at_stage must be null when terminal_state is completed');
775
+ }
776
+ } else if (input.stopped_at_stage !== undefined && input.stopped_at_stage !== null) {
777
+ if (checkEnum(errors, 'stopped_at_stage', input.stopped_at_stage, STAGES)) {
778
+ stoppedAtStage = input.stopped_at_stage;
779
+ }
780
+ } else {
781
+ stoppedAtStage = context.openStage || context.lastStage || 'config';
782
+ }
783
+
784
+ let operatorAcceptance = 'unknown';
785
+ if (input.operator_acceptance !== undefined && input.operator_acceptance !== null) {
786
+ if (checkEnum(errors, 'operator_acceptance', input.operator_acceptance, OPERATOR_ACCEPTANCE)) {
787
+ operatorAcceptance = input.operator_acceptance;
788
+ }
789
+ }
790
+
791
+ const volumes = validateVolumes(errors, input.volumes);
792
+ const verification = validateVerification(errors, input.verification);
793
+
794
+ let dims = {};
795
+ try {
796
+ dims = validateDims(dimKeys);
797
+ } catch (error) {
798
+ errors.push(...error.errors);
799
+ }
800
+
801
+ if (errors.length > 0) throw new ValidationError(errors);
802
+ return {
803
+ terminal_state: input.terminal_state,
804
+ stopped_at_stage: stoppedAtStage,
805
+ operator_acceptance: operatorAcceptance,
806
+ volumes,
807
+ verification,
808
+ dims,
809
+ };
810
+ }
811
+
812
+ // --- journal ------------------------------------------------------------------
813
+
814
+ function readJournalFile(file) {
815
+ const records = [];
816
+ let malformed = 0;
817
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
818
+ if (line.trim() === '') continue;
819
+ try {
820
+ records.push(JSON.parse(line));
821
+ } catch {
822
+ malformed += 1;
823
+ }
824
+ }
825
+ return { records, malformed };
826
+ }
827
+
828
+ function readJournal(projectDir) {
829
+ const file = journalPath(projectDir);
830
+ if (!fs.existsSync(file)) return null;
831
+ return readJournalFile(file);
832
+ }
833
+
834
+ function appendJournal(projectDir, record) {
835
+ const file = journalPath(projectDir);
836
+ fs.mkdirSync(path.dirname(file), { recursive: true });
837
+ fs.appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf8');
838
+ }
839
+
840
+ function replay(records) {
841
+ const state = {
842
+ runStart: null,
843
+ dims: {},
844
+ sessionCount: 1,
845
+ seq: 0,
846
+ events: [],
847
+ rejected: [],
848
+ entries: [],
849
+ waits: [],
850
+ meters: [],
851
+ openStage: null,
852
+ openStageStart: null,
853
+ lastStage: null,
854
+ openWait: null,
855
+ scrubHits: 0,
856
+ biPushFailures: 0,
857
+ biPushTruncated: 0,
858
+ finalize: null,
859
+ lastTs: null,
860
+ };
861
+ for (const record of records) {
862
+ state.seq = Math.max(state.seq, record.seq || 0);
863
+ state.lastTs = record.ts || state.lastTs;
864
+ switch (record.type) {
865
+ case 'run_start':
866
+ state.runStart = record;
867
+ Object.assign(state.dims, record.dims || {});
868
+ break;
869
+ case 'session_start':
870
+ state.sessionCount = record.session;
871
+ break;
872
+ case 'dims':
873
+ Object.assign(state.dims, record.dims || {});
874
+ break;
875
+ case 'stage_start':
876
+ state.openStage = record.stage;
877
+ state.openStageStart = record.ts;
878
+ state.lastStage = record.stage;
879
+ break;
880
+ case 'stage_end':
881
+ if (state.openStage === record.stage) {
882
+ state.entries.push({ stage: record.stage, start: state.openStageStart, end: record.ts, outcome: record.outcome });
883
+ state.openStage = null;
884
+ state.openStageStart = null;
885
+ }
886
+ break;
887
+ case 'wait_start':
888
+ state.openWait = { stage: record.stage, start: record.ts };
889
+ break;
890
+ case 'wait_end':
891
+ if (state.openWait) {
892
+ state.waits.push({ ...state.openWait, end: record.ts });
893
+ state.openWait = null;
894
+ }
895
+ break;
896
+ case 'meter':
897
+ // Accumulate: a stage may be entered more than once, and each generated script reports its
898
+ // own invocation. Summing is the only reading that survives a resume.
899
+ state.meters.push(record.meter);
900
+ break;
901
+ case 'event':
902
+ state.events.push(record);
903
+ state.scrubHits += record.scrub_hits || 0;
904
+ break;
905
+ case 'rejected':
906
+ state.rejected.push(record);
907
+ break;
908
+ case 'bi_push_failed':
909
+ state.biPushFailures += 1;
910
+ break;
911
+ case 'bi_push_truncated':
912
+ state.biPushTruncated += record.rows || 1;
913
+ break;
914
+ case 'finalize':
915
+ state.finalize = record;
916
+ break;
917
+ default:
918
+ break;
919
+ }
920
+ }
921
+ return state;
922
+ }
923
+
924
+ function requireActiveRun(projectDir) {
925
+ const journal = readJournal(projectDir);
926
+ if (!journal) {
927
+ throw new ValidationError(
928
+ ['no active telemetry run in this project'],
929
+ "call `rp-telemetry.js start '<dims-json>'` first",
930
+ );
931
+ }
932
+ const state = replay(journal.records);
933
+ if (!state.runStart) {
934
+ throw new ValidationError(['telemetry journal is corrupt: missing run-start record']);
935
+ }
936
+ if (state.finalize) {
937
+ // A crash after the finalize append but before archival: finish the archival
938
+ // now, then report no active run.
939
+ archiveFinalizedJournal(projectDir, journal, state);
940
+ throw new ValidationError(
941
+ ['the previous run is already finalized'],
942
+ "call `rp-telemetry.js start '<dims-json>'` to open the next run",
943
+ );
944
+ }
945
+ return { journal, state };
946
+ }
947
+
948
+ // --- fold + document assembly ---------------------------------------------------
949
+
950
+ function foldEvents(eventRecords, runId) {
951
+ const byClass = new Map();
952
+ for (const record of eventRecords) {
953
+ const e = record.event;
954
+ let folded = byClass.get(record.class_key);
955
+ if (!folded) {
956
+ folded = {
957
+ run_id: runId,
958
+ seq: record.seq,
959
+ count: 0,
960
+ stage: e.stage,
961
+ skill: e.skill,
962
+ event_type: e.event_type,
963
+ subtype: e.subtype,
964
+ entity_type: e.entity_type,
965
+ severity: e.severity,
966
+ wix_api_surface: e.wix_api_surface,
967
+ source_api_surface: e.source_api_surface,
968
+ wix_app_id: e.wix_app_id,
969
+ error_code: e.error_code,
970
+ what_happened: e.what_happened,
971
+ expected: e.expected,
972
+ actual: e.actual,
973
+ observed_shapes: null,
974
+ evidence_refs: null,
975
+ _shapes: new Map(),
976
+ _refs: new Map(),
977
+ };
978
+ if (e.event_type === 'error') {
979
+ folded.retry_count = 0;
980
+ folded.recovered = false;
981
+ }
982
+ if (e.event_type === 'user_decision') folded.decision_point = e.decision_point;
983
+ byClass.set(record.class_key, folded);
984
+ }
985
+ folded.count += e.count;
986
+ if (e.event_type === 'error') {
987
+ folded.retry_count += e.retry_count || 0;
988
+ // A retry chain is often recorded per attempt. The latest occurrence knows
989
+ // whether the operation ultimately succeeded, and a recovering occurrence's
990
+ // `actual` carries the change that made it succeed — the first (failing)
991
+ // attempt's fields must never erase that half of the observation.
992
+ folded.recovered = Boolean(e.recovered);
993
+ if (e.recovered && e.actual) folded.actual = e.actual;
994
+ }
995
+ for (const shape of e.observed_shapes || []) {
996
+ const key = stableStringify(shape);
997
+ if (!folded._shapes.has(key)) folded._shapes.set(key, shape);
998
+ }
999
+ for (const ref of e.evidence_refs || []) {
1000
+ const key = `${ref.artifact}#${ref.locator || ''}`;
1001
+ if (!folded._refs.has(key)) folded._refs.set(key, ref);
1002
+ }
1003
+ }
1004
+ const events = [...byClass.values()].sort((a, b) => a.seq - b.seq);
1005
+ for (const event of events) {
1006
+ const shapes = [...event._shapes.values()];
1007
+ if (shapes.length > 0) {
1008
+ event.observed_shapes = shapes.slice(0, SHAPES_KEPT_PER_CLASS);
1009
+ // Surfaced bound, never a silent one.
1010
+ if (shapes.length > SHAPES_KEPT_PER_CLASS) event.shapes_seen = shapes.length;
1011
+ }
1012
+ const refs = [...event._refs.values()];
1013
+ if (refs.length > 0) event.evidence_refs = refs.slice(0, EVIDENCE_REFS_MAX);
1014
+ delete event._shapes;
1015
+ delete event._refs;
1016
+ }
1017
+ return events;
1018
+ }
1019
+
1020
+ // Event types that mean the stage did not run clean. `contained_recovery` is DERIVED from these
1021
+ // rather than self-reported, because a stage that spent its time debugging is exactly the stage an
1022
+ // agent is least likely to remember to flag.
1023
+ const RECOVERY_EVENT_TYPES = new Set(['error', 'pipeline_defect']);
1024
+
1025
+ function computeStages(state, finalizeTs) {
1026
+ const entries = [...state.entries];
1027
+ const waits = [...state.waits];
1028
+ if (state.openWait) {
1029
+ waits.push({ ...state.openWait, end: finalizeTs });
1030
+ }
1031
+ const byStage = new Map();
1032
+ const order = [];
1033
+ const stageRecord = (stage) => {
1034
+ if (!byStage.has(stage)) {
1035
+ byStage.set(stage, { stage, outcome: 'halted', active_ms: 0, waiting_ms: 0, _entries: [] });
1036
+ order.push(stage);
1037
+ }
1038
+ return byStage.get(stage);
1039
+ };
1040
+ for (const entry of entries) {
1041
+ const record = stageRecord(entry.stage);
1042
+ record.outcome = entry.outcome;
1043
+ record._entries.push(entry);
1044
+ }
1045
+ for (const wait of waits) {
1046
+ const record = stageRecord(wait.stage || 'config');
1047
+ record.waiting_ms += Math.max(0, tsMs(wait.end) - tsMs(wait.start));
1048
+ }
1049
+ // Metered measurements, summed per stage. A stage named by a meter but never opened still gets a
1050
+ // record, so a mis-attributed meter is visible instead of silently discarded.
1051
+ for (const meter of state.meters || []) {
1052
+ const record = stageRecord(meter.stage);
1053
+ for (const key of [...METER_DURATION_KEYS, ...METER_COUNT_KEYS]) {
1054
+ if (meter[key] === undefined) continue;
1055
+ record[key] = (record[key] || 0) + meter[key];
1056
+ }
1057
+ }
1058
+
1059
+ // Stages that contained an error or a pipeline defect. Without this, a clean run and a thrash are
1060
+ // indistinguishable in the rollup, and every per-stage duration silently includes repair time.
1061
+ const recoveryStages = new Set(
1062
+ (state.events || [])
1063
+ .filter((e) => RECOVERY_EVENT_TYPES.has(e.event && e.event.event_type ? e.event.event_type : e.event_type))
1064
+ .map((e) => (e.event && e.event.stage ? e.event.stage : e.stage))
1065
+ .filter(Boolean),
1066
+ );
1067
+
1068
+ for (const stage of order) {
1069
+ const record = byStage.get(stage);
1070
+ let active = 0;
1071
+ for (const entry of record._entries) {
1072
+ active += Math.max(0, tsMs(entry.end) - tsMs(entry.start));
1073
+ for (const wait of waits) {
1074
+ if ((wait.stage || 'config') !== stage) continue;
1075
+ const overlap = Math.min(tsMs(entry.end), tsMs(wait.end)) - Math.max(tsMs(entry.start), tsMs(wait.start));
1076
+ if (overlap > 0) active -= overlap;
1077
+ }
1078
+ }
1079
+ record.active_ms = Math.max(0, active);
1080
+ record.contained_recovery = recoveryStages.has(stage);
1081
+ // The remainder of active_ms that no meter explained. This is the honesty field: it is how much
1082
+ // of the stage is still a black box, and it must never be silently folded into a component.
1083
+ const attributed = METER_DURATION_KEYS.reduce((sum, key) => sum + (record[key] || 0), 0);
1084
+ record.unattributed_ms = Math.max(0, record.active_ms - attributed);
1085
+ delete record._entries;
1086
+ }
1087
+ return order.map((stage) => byStage.get(stage));
1088
+ }
1089
+
1090
+ // Cost is DERIVED from token counts and the pricing snapshot, never recorded as a figure — a stored
1091
+ // dollar amount silently goes wrong the moment list prices change, while tokens are a fact.
1092
+ function computeCost(stages, pricingSnapshot, model) {
1093
+ const totals = { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0 };
1094
+ for (const stage of stages) {
1095
+ for (const key of Object.keys(totals)) totals[key] += stage[key] || 0;
1096
+ }
1097
+ const anyTokens = Object.values(totals).some((v) => v > 0);
1098
+ const rates = pricingSnapshot && model ? pricingSnapshot[model] : null;
1099
+ if (!anyTokens || !rates) {
1100
+ return { tokens: totals, estimated_cost_usd: null, cost_basis: rates ? 'no_tokens_recorded' : 'no_pricing_snapshot' };
1101
+ }
1102
+ const perM = (count, rate) => (rate === undefined ? 0 : (count / 1e6) * rate);
1103
+ const usd = perM(totals.input_tokens, rates.input_per_mtok)
1104
+ + perM(totals.output_tokens, rates.output_per_mtok)
1105
+ + perM(totals.cache_read_tokens, rates.cache_read_per_mtok)
1106
+ + perM(totals.cache_write_tokens, rates.cache_write_per_mtok);
1107
+ return { tokens: totals, estimated_cost_usd: Number(usd.toFixed(6)), cost_basis: 'derived_from_snapshot' };
1108
+ }
1109
+
1110
+ const MANIFEST_EXCLUDE_DIRS = new Set(['config', 'data', 'preview', 'node_modules', 'frontend', 'telemetry']);
1111
+ // .ndjson: the pipeline's audit logs (logs/audit-*.ndjson) are spec-named
1112
+ // evidence — a pipeline_defect deep-dive depends on them being in the bundle.
1113
+ const MANIFEST_EXTENSIONS = new Set(['.md', '.json', '.jsonl', '.ndjson', '.js', '.mjs', '.cjs', '.ts', '.sh', '.yaml', '.yml']);
1114
+
1115
+ function walkEvidence(projectDir) {
1116
+ const files = [];
1117
+ const walk = (rel) => {
1118
+ const abs = rel === '' ? projectDir : path.join(projectDir, rel);
1119
+ const entries = fs.readdirSync(abs, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1120
+ for (const entry of entries) {
1121
+ if (entry.name.startsWith('.')) continue;
1122
+ const relPath = rel === '' ? entry.name : `${rel}/${entry.name}`;
1123
+ if (entry.isDirectory()) {
1124
+ if (MANIFEST_EXCLUDE_DIRS.has(entry.name)) continue;
1125
+ walk(relPath);
1126
+ continue;
1127
+ }
1128
+ if (!entry.isFile()) continue;
1129
+ if (entry.name.endsWith('.env')) continue;
1130
+ if (relPath === 'run-telemetry.json') continue; // added explicitly, first
1131
+ if (!MANIFEST_EXTENSIONS.has(path.extname(entry.name))) continue;
1132
+ files.push({ path: relPath, bytes: fs.statSync(path.join(projectDir, relPath)).size });
1133
+ }
1134
+ };
1135
+ walk('');
1136
+ return files;
1137
+ }
1138
+
1139
+ function assembleDocument(projectDir, records, malformedLines = 0) {
1140
+ const state = replay(records);
1141
+ state.malformedLines = malformedLines;
1142
+ const runStart = state.runStart;
1143
+ const payload = state.finalize.payload;
1144
+ const finalizeTs = state.finalize.ts;
1145
+ const dims = { ...state.dims, ...payload.dims };
1146
+
1147
+ const events = foldEvents(state.events, runStart.run_id);
1148
+ const stages = computeStages(state, finalizeTs);
1149
+ const activeMs = stages.reduce((sum, s) => sum + s.active_ms, 0);
1150
+ const waitingMs = stages.reduce((sum, s) => sum + s.waiting_ms, 0);
1151
+
1152
+ // The headline question this schema exists to answer: how much of the run was deterministic code
1153
+ // versus model reasoning. `unattributed_ms` is reported alongside so the split is never mistaken
1154
+ // for a complete accounting of active_ms.
1155
+ const sumOver = (key) => stages.reduce((sum, s) => sum + (s[key] || 0), 0);
1156
+ const agenticMs = sumOver('model_ms');
1157
+ const deterministicMs = sumOver('script_ms') + sumOver('api_ms');
1158
+ const unattributedMs = sumOver('unattributed_ms');
1159
+ const recoveryStages = stages.filter((s) => s.contained_recovery).map((s) => s.stage);
1160
+ const cost = computeCost(
1161
+ stages,
1162
+ dims.model_pricing_snapshot || null,
1163
+ (runStart.runtime_env && runStart.runtime_env.model) || (dims.runtime_env && dims.runtime_env.model) || null,
1164
+ );
1165
+ const timing = {
1166
+ active_ms: activeMs,
1167
+ waiting_ms: waitingMs,
1168
+ agentic_ms: agenticMs,
1169
+ deterministic_ms: deterministicMs,
1170
+ unattributed_ms: unattributedMs,
1171
+ // What fraction of the EXPLAINED time was the model. Null when nothing was metered, rather
1172
+ // than a misleading 0.
1173
+ agentic_share: agenticMs + deterministicMs > 0
1174
+ ? Number((agenticMs / (agenticMs + deterministicMs)).toFixed(4))
1175
+ : null,
1176
+ api_calls: sumOver('api_calls'),
1177
+ api_retries: sumOver('api_retries'),
1178
+ stages_with_recovery: recoveryStages,
1179
+ };
1180
+
1181
+ const eventCountByType = {};
1182
+ for (const event of events) {
1183
+ const entry = eventCountByType[event.event_type] || { classes: 0, occurrences: 0 };
1184
+ entry.classes += 1;
1185
+ entry.occurrences += event.count;
1186
+ eventCountByType[event.event_type] = entry;
1187
+ }
1188
+
1189
+ const acceptedClasses = new Set(state.events.map((r) => r.class_key));
1190
+ const dropped = state.rejected.filter((r) => !r.class_key || !acceptedClasses.has(r.class_key)).length;
1191
+
1192
+ const flags = [];
1193
+ const hasEventType = (...types) => events.some((e) => types.includes(e.event_type));
1194
+ if (payload.volumes.some((v) => v.failed > 0) && !hasEventType('error')) {
1195
+ flags.push('volumes_report_failures_but_no_error_events');
1196
+ }
1197
+ for (const volume of payload.volumes) {
1198
+ if (volume.planned !== null && volume.attempted > volume.planned + volume.already_imported) {
1199
+ // Execution diverging from the approved plan must surface mechanically.
1200
+ flags.push(`attempted_exceeds_planned:${volume.entity_type}`);
1201
+ }
1202
+ if (volume.attempted + volume.skipped + volume.already_imported > volume.discovered) {
1203
+ flags.push(`volumes_exceed_discovered:${volume.entity_type}`);
1204
+ }
1205
+ }
1206
+ // Metering flags cover data that is WRONG, never data that is merely ABSENT.
1207
+ //
1208
+ // A run that meters nothing is not unhealthy — it is a runtime that does not report usage, which
1209
+ // describes every run recorded before metering existed. Flagging those would fire on ~all
1210
+ // historical runs and train readers to ignore the flag list. Absence is already visible, and
1211
+ // queryable, in `timing`: `agentic_share` is null and `unattributed_ms` carries the whole stage.
1212
+ // So only contradictions are flagged here, and each one implies metering was in use.
1213
+ for (const stage of stages) {
1214
+ const attributed = METER_DURATION_KEYS.reduce((sum, key) => sum + (stage[key] || 0), 0);
1215
+ if (attributed === 0) continue;
1216
+ // Partially metered is more dangerous than not metered at all: it looks explained.
1217
+ if (stage.unattributed_ms > stage.active_ms * 0.5) {
1218
+ flags.push(`stage_mostly_unattributed:${stage.stage}`);
1219
+ }
1220
+ // Attributed time exceeding elapsed time is impossible, so it means the measurements are wrong:
1221
+ // usually the same interval metered twice across a resume, or concurrent work summed serially.
1222
+ // unattributed_ms clamps at 0, so without this flag the contradiction would be invisible.
1223
+ if (attributed > stage.active_ms * OVER_ATTRIBUTION_TOLERANCE + OVER_ATTRIBUTION_FLOOR_MS) {
1224
+ flags.push(`stage_over_attributed:${stage.stage}`);
1225
+ }
1226
+ }
1227
+ if (cost.cost_basis === 'no_pricing_snapshot' && (cost.tokens.input_tokens > 0 || cost.tokens.output_tokens > 0)) {
1228
+ flags.push('tokens_recorded_without_pricing_snapshot');
1229
+ }
1230
+ if (payload.verification.some((v) => v.failed > 0) && !hasEventType('error', 'fidelity_loss')) {
1231
+ flags.push('verification_failures_but_no_matching_events');
1232
+ }
1233
+ if (state.malformedLines > 0) {
1234
+ flags.push(`journal_malformed_lines:${state.malformedLines}`);
1235
+ }
1236
+ if (state.biPushTruncated > 0) {
1237
+ flags.push(`bi_push_truncated:${state.biPushTruncated}`);
1238
+ }
1239
+ // A stalled wait whose stage has no halt_needs_user event lost the "why" of
1240
+ // the stall — the single most actionable signal for where runs stall.
1241
+ const haltStages = new Set(events.filter((e) => e.event_type === 'halt_needs_user').map((e) => e.stage));
1242
+ const allWaits = [...state.waits, ...(state.openWait ? [{ ...state.openWait, end: finalizeTs }] : [])];
1243
+ const flaggedWaitStages = new Set();
1244
+ for (const w of allWaits) {
1245
+ const waitStage = w.stage || 'config';
1246
+ if (tsMs(w.end) - tsMs(w.start) < WAIT_WITHOUT_HALT_FLAG_MS) continue;
1247
+ if (haltStages.has(waitStage) || flaggedWaitStages.has(waitStage)) continue;
1248
+ flaggedWaitStages.add(waitStage);
1249
+ flags.push(`wait_without_halt_event:${waitStage}`);
1250
+ }
1251
+ // Discovery is where extension classes become known; a null left after a
1252
+ // passed discovery usually means the dims call was skipped, not that the
1253
+ // source has no extensions (stamp [] explicitly for that).
1254
+ const discoveryPassed = state.entries.some((e) => e.stage === 'discovery' && e.outcome === 'passed');
1255
+ if (discoveryPassed && (dims.source_extensions === undefined || dims.source_extensions === null)
1256
+ && dims.source_platform !== 'csv') {
1257
+ flags.push('source_extensions_null_after_discovery');
1258
+ }
1259
+
1260
+ const evidence = walkEvidence(projectDir);
1261
+ const evidenceBytes = evidence.reduce((sum, f) => sum + f.bytes, 0);
1262
+ if (evidenceBytes > EVIDENCE_SIZE_FLAG_BYTES) {
1263
+ flags.push(`evidence_unusually_large:${Math.round(evidenceBytes / 1024)}kb`);
1264
+ }
1265
+
1266
+ const rollup = {
1267
+ run_id: runStart.run_id,
1268
+ project_id: runStart.project_id,
1269
+ attempt: runStart.attempt,
1270
+ session_count: state.sessionCount,
1271
+ schema_version: TELEMETRY_SCHEMA_VERSION,
1272
+ skills_version: dims.skills_version || runStart.skills_version || null,
1273
+ skills_commit: dims.skills_commit || runStart.skills_commit || null,
1274
+ runtime_env: runStart.runtime_env || dims.runtime_env
1275
+ ? { ...(runStart.runtime_env || {}), ...(dims.runtime_env || {}) }
1276
+ : null,
1277
+ site_id: dims.site_id ?? null,
1278
+ wix_user_id: dims.wix_user_id ?? null,
1279
+ source_platform: dims.source_platform ?? null,
1280
+ source_platform_version: dims.source_platform_version ?? null,
1281
+ source_site_url: dims.source_site_url ?? null,
1282
+ source_extensions: dims.source_extensions ?? null,
1283
+ source_acquisition: dims.source_acquisition ?? null,
1284
+ delivery_mode: dims.delivery_mode ?? null,
1285
+ destination_strategy: dims.destination_strategy ?? null,
1286
+ terminal_state: payload.terminal_state,
1287
+ stopped_at_stage: payload.stopped_at_stage,
1288
+ stages,
1289
+ volumes: payload.volumes,
1290
+ verification: payload.verification,
1291
+ operator_acceptance: payload.operator_acceptance,
1292
+ telemetry_health: {
1293
+ rejected_calls: state.rejected.length,
1294
+ dropped_events: dropped,
1295
+ scrub_hits: state.scrubHits,
1296
+ bi_push_failures: state.biPushFailures,
1297
+ flags,
1298
+ },
1299
+ event_count_by_type: eventCountByType,
1300
+ active_ms: activeMs,
1301
+ waiting_ms: waitingMs,
1302
+ timing,
1303
+ cost,
1304
+ model_pricing_snapshot: dims.model_pricing_snapshot ?? null,
1305
+ run_started: runStart.ts,
1306
+ run_ended: finalizeTs,
1307
+ bundle_manifest: ['run-telemetry.json', ...evidence.map((f) => f.path)],
1308
+ };
1309
+
1310
+ return {
1311
+ document: {
1312
+ schema_version: TELEMETRY_SCHEMA_VERSION,
1313
+ field_tiers: FIELD_TIERS,
1314
+ rollup,
1315
+ events,
1316
+ },
1317
+ evidenceBytes,
1318
+ evidenceFiles: evidence,
1319
+ };
1320
+ }
1321
+
1322
+ function writeJsonAtomic(filePath, data) {
1323
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1324
+ const tempPath = `${filePath}.${process.pid}.tmp`;
1325
+ fs.writeFileSync(tempPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
1326
+ fs.renameSync(tempPath, filePath);
1327
+ }
1328
+
1329
+ function archiveName(state) {
1330
+ return `run-${state.runStart.attempt}-${state.runStart.run_id}`;
1331
+ }
1332
+
1333
+ function archiveFinalizedJournal(projectDir, journal, state) {
1334
+ // Deterministic completion of a finalize: rebuild the document from the
1335
+ // journal (idempotent), write signal + archive copies, move the journal.
1336
+ const assembled = assembleDocument(projectDir, journal.records, journal.malformed);
1337
+ const base = archiveName(state);
1338
+ fs.mkdirSync(runsDir(projectDir), { recursive: true });
1339
+ writeJsonAtomic(signalPath(projectDir), assembled.document);
1340
+ writeJsonAtomic(path.join(runsDir(projectDir), `${base}.json`), assembled.document);
1341
+ fs.renameSync(journalPath(projectDir), path.join(runsDir(projectDir), `${base}.jsonl`));
1342
+ return assembled;
1343
+ }
1344
+
1345
+ function lastFinalizedAttempt(projectDir) {
1346
+ let last = 0;
1347
+ const signal = signalPath(projectDir);
1348
+ if (fs.existsSync(signal)) {
1349
+ try {
1350
+ const doc = JSON.parse(fs.readFileSync(signal, 'utf8'));
1351
+ if (doc && doc.rollup && Number.isInteger(doc.rollup.attempt)) last = Math.max(last, doc.rollup.attempt);
1352
+ } catch {
1353
+ // unreadable prior signal file — fall through to the archive scan
1354
+ }
1355
+ }
1356
+ const dir = runsDir(projectDir);
1357
+ if (fs.existsSync(dir)) {
1358
+ for (const name of fs.readdirSync(dir)) {
1359
+ const match = /^run-(\d+)-/.exec(name);
1360
+ if (match) last = Math.max(last, Number(match[1]));
1361
+ }
1362
+ }
1363
+ return last;
1364
+ }
1365
+
1366
+ function resolveSkillsVersion() {
1367
+ let dir = __dirname;
1368
+ for (let depth = 0; depth < 7; depth += 1) {
1369
+ const candidate = path.join(dir, 'VERSION');
1370
+ if (fs.existsSync(candidate)) {
1371
+ const version = fs.readFileSync(candidate, 'utf8').trim();
1372
+ if (/^\d+\.\d+\.\d+$/.test(version)) return version;
1373
+ }
1374
+ const parent = path.dirname(dir);
1375
+ if (parent === dir) break;
1376
+ dir = parent;
1377
+ }
1378
+ return null;
1379
+ }
1380
+
1381
+ function resolveSkillsCommit() {
1382
+ // The exact source commit behind this bundle — the drill-down provenance the
1383
+ // coarse, hand-bumped `skills_version` (semver) cannot give. Two sources, in
1384
+ // order of authority:
1385
+ //
1386
+ // 1. `.publish-manifest.json`, stamped by publish-skills-to-wix.sh next to
1387
+ // VERSION at publish time. This is the only source that works in a partner
1388
+ // runtime (no git there) AND the only correct one once the bundle lives
1389
+ // inside the consuming wix/skills repo — where a git probe would return
1390
+ // that repo's HEAD, not the replatform source commit. So it wins.
1391
+ // 2. Dev-mode git fallback: runs straight from the replatform checkout, where
1392
+ // the recorder's own directory is inside the source repo, have no manifest
1393
+ // but do have git — so `git -C __dirname rev-parse --short HEAD` is the source
1394
+ // commit. Never reached once a manifest is present (case 1).
1395
+ let dir = __dirname;
1396
+ for (let depth = 0; depth < 7; depth += 1) {
1397
+ const candidate = path.join(dir, '.publish-manifest.json');
1398
+ if (fs.existsSync(candidate)) {
1399
+ try {
1400
+ const manifest = JSON.parse(fs.readFileSync(candidate, 'utf8'));
1401
+ if (typeof manifest.sourceCommit === 'string' && /^[0-9a-f]{7,40}$/.test(manifest.sourceCommit)) {
1402
+ return manifest.sourceCommit;
1403
+ }
1404
+ } catch {
1405
+ // unreadable manifest — fall through to the dev-mode git probe
1406
+ }
1407
+ }
1408
+ const parent = path.dirname(dir);
1409
+ if (parent === dir) break;
1410
+ dir = parent;
1411
+ }
1412
+ try {
1413
+ const sha = execFileSync('git', ['-C', __dirname, 'rev-parse', '--short', 'HEAD'], {
1414
+ encoding: 'utf8',
1415
+ stdio: ['ignore', 'pipe', 'ignore'],
1416
+ }).trim();
1417
+ if (/^[0-9a-f]{7,40}$/.test(sha)) return sha;
1418
+ } catch {
1419
+ // no git, not a repo, or git unavailable — commit provenance stays null
1420
+ }
1421
+ return null;
1422
+ }
1423
+
1424
+ // --- BI sink (spec 0007) ----------------------------------------------------------
1425
+
1426
+ // Push rows to BI and journal any failures/truncation so telemetry loss is
1427
+ // visible in telemetry_health, never silent. Never throws — BI unreachable,
1428
+ // slow, or rejecting is a telemetry failure, not a run failure. Returns the
1429
+ // last journal seq used plus sent/failed row counts.
1430
+ async function pushToBi(projectDir, ts, seq, rows, loggedUserId, target) {
1431
+ const result = await biSink.pushRows(rows, loggedUserId);
1432
+ if (result.truncated > 0) {
1433
+ seq += 1;
1434
+ appendJournal(projectDir, { type: 'bi_push_truncated', ts, seq, target, rows: result.truncated });
1435
+ }
1436
+ for (const failure of result.failures) {
1437
+ seq += 1;
1438
+ appendJournal(projectDir, {
1439
+ type: 'bi_push_failed', ts, seq, target, rows: failure.rows, detail: failure.detail,
1440
+ });
1441
+ }
1442
+ return {
1443
+ seq,
1444
+ sent: result.sent,
1445
+ failed: result.failures.reduce((sum, f) => sum + f.rows, 0),
1446
+ };
1447
+ }
1448
+
1449
+ // The folded BI row for one event class: prior journal records of the class plus
1450
+ // the just-appended one. Re-pushes of a growing class are safe — the reviewer
1451
+ // dedupes on (run_id, seq), latest row wins with the cumulative count.
1452
+ function foldedRowForClass(state, eventRecord) {
1453
+ const classRecords = [
1454
+ ...state.events.filter((r) => r.class_key === eventRecord.class_key),
1455
+ eventRecord,
1456
+ ];
1457
+ return biSink.eventRow(foldEvents(classRecords, state.runStart.run_id)[0]);
1458
+ }
1459
+
1460
+ // --- public command API ---------------------------------------------------------
1461
+
1462
+ async function start(projectDir, dimsInput, { now } = {}) {
1463
+ const ts = nowIso(now);
1464
+ const dims = validateDims(dimsInput);
1465
+ const journal = readJournal(projectDir);
1466
+
1467
+ if (journal) {
1468
+ const state = replay(journal.records);
1469
+ if (!state.runStart) {
1470
+ throw new ValidationError(['telemetry journal is corrupt: missing run-start record']);
1471
+ }
1472
+ if (state.finalize) {
1473
+ archiveFinalizedJournal(projectDir, journal, state);
1474
+ } else {
1475
+ // Resume: same run_id, same attempt, one more session. Cross-session user
1476
+ // latency lands in waiting_ms, never in a phantom second run.
1477
+ let seq = state.seq;
1478
+ if (state.openWait) {
1479
+ seq += 1;
1480
+ appendJournal(projectDir, { type: 'wait_end', ts, seq });
1481
+ } else if (state.lastTs && tsMs(ts) - tsMs(state.lastTs) >= IMPLICIT_WAIT_MIN_MS) {
1482
+ // The run stopped without an open wait (e.g. a crash); the dead interval
1483
+ // is waiting time, not active time.
1484
+ const stage = state.openStage || state.lastStage || 'config';
1485
+ seq += 1;
1486
+ appendJournal(projectDir, { type: 'wait_start', ts: state.lastTs, seq, stage, imputed: true });
1487
+ seq += 1;
1488
+ appendJournal(projectDir, { type: 'wait_end', ts, seq, imputed: true });
1489
+ }
1490
+ seq += 1;
1491
+ appendJournal(projectDir, { type: 'session_start', ts, seq, session: state.sessionCount + 1 });
1492
+ if (Object.keys(dims).length > 0) {
1493
+ seq += 1;
1494
+ appendJournal(projectDir, { type: 'dims', ts, seq, dims });
1495
+ }
1496
+ // Operator identity arriving on a resume means the phase:started row could
1497
+ // not have been sent yet — send it now (dedup makes a re-send harmless).
1498
+ if (biSink.isGuid(dims.wix_user_id)) {
1499
+ await pushToBi(projectDir, ts, seq,
1500
+ [biSink.startedRunRow(state.runStart, dims.wix_user_id)], dims.wix_user_id, 'run_started');
1501
+ }
1502
+ return {
1503
+ resumed: true,
1504
+ run_id: state.runStart.run_id,
1505
+ attempt: state.runStart.attempt,
1506
+ session_count: state.sessionCount + 1,
1507
+ open_stage: state.openStage,
1508
+ };
1509
+ }
1510
+ }
1511
+
1512
+ const attempt = lastFinalizedAttempt(projectDir) + 1;
1513
+ const runtimeEnv = {
1514
+ os: `${os.platform()} ${os.release()}`,
1515
+ node_version: process.version,
1516
+ agent_runtime: (dims.runtime_env && dims.runtime_env.agent_runtime) || null,
1517
+ model: (dims.runtime_env && dims.runtime_env.model) || null,
1518
+ ...(dims.runtime_env || {}),
1519
+ };
1520
+ delete dims.runtime_env;
1521
+ const skillsVersion = dims.skills_version || resolveSkillsVersion();
1522
+ delete dims.skills_version;
1523
+ const skillsCommit = dims.skills_commit || resolveSkillsCommit();
1524
+ delete dims.skills_commit;
1525
+
1526
+ const record = {
1527
+ type: 'run_start',
1528
+ ts,
1529
+ seq: 1,
1530
+ run_id: crypto.randomUUID(),
1531
+ project_id: path.basename(path.resolve(projectDir)),
1532
+ attempt,
1533
+ session: 1,
1534
+ schema_version: TELEMETRY_SCHEMA_VERSION,
1535
+ skills_version: skillsVersion,
1536
+ skills_commit: skillsCommit,
1537
+ runtime_env: runtimeEnv,
1538
+ dims,
1539
+ };
1540
+ appendJournal(projectDir, record);
1541
+ // Emit `replatform_run` phase:started (spec 0007). Without a GUID operator
1542
+ // identity there is no BI route yet — a later dims call carrying wix_user_id
1543
+ // (or the finalize re-push) sends it then.
1544
+ if (biSink.isGuid(dims.wix_user_id)) {
1545
+ await pushToBi(projectDir, ts, record.seq,
1546
+ [biSink.startedRunRow(record, dims.wix_user_id)], dims.wix_user_id, 'run_started');
1547
+ }
1548
+ return {
1549
+ resumed: false,
1550
+ run_id: record.run_id,
1551
+ project_id: record.project_id,
1552
+ attempt,
1553
+ session_count: 1,
1554
+ skills_version: skillsVersion,
1555
+ skills_commit: skillsCommit,
1556
+ };
1557
+ }
1558
+
1559
+ async function dims(projectDir, dimsInput, { now } = {}) {
1560
+ const ts = nowIso(now);
1561
+ const { state } = requireActiveRun(projectDir);
1562
+ const validated = validateDims(dimsInput);
1563
+ if (Object.keys(validated).length === 0) {
1564
+ throw new ValidationError(['dims payload contains no known dimension fields']);
1565
+ }
1566
+ appendJournal(projectDir, { type: 'dims', ts, seq: state.seq + 1, dims: validated });
1567
+ // Identity becoming known mid-run unlocks the BI route: send the
1568
+ // phase:started row that start() had to hold back.
1569
+ if (biSink.isGuid(validated.wix_user_id)) {
1570
+ await pushToBi(projectDir, ts, state.seq + 1,
1571
+ [biSink.startedRunRow(state.runStart, validated.wix_user_id)], validated.wix_user_id, 'run_started');
1572
+ }
1573
+ return { recorded: Object.keys(validated) };
1574
+ }
1575
+
1576
+ async function record(projectDir, eventInput, { now } = {}) {
1577
+ const ts = nowIso(now);
1578
+ const { state } = requireActiveRun(projectDir);
1579
+ let validated;
1580
+ try {
1581
+ validated = validateEvent(eventInput, state);
1582
+ } catch (error) {
1583
+ if (error instanceof ValidationError) {
1584
+ // The schema gate refused the call: count it so telemetry loss is never
1585
+ // silent, then surface the errors for a retry.
1586
+ appendJournal(projectDir, {
1587
+ type: 'rejected',
1588
+ ts,
1589
+ seq: state.seq + 1,
1590
+ class_key: error.hint || null,
1591
+ reasons: error.errors,
1592
+ });
1593
+ error.hint = 'fix the listed fields and retry the record call';
1594
+ }
1595
+ throw error;
1596
+ }
1597
+ const eventRecord = {
1598
+ type: 'event',
1599
+ ts,
1600
+ seq: state.seq + 1,
1601
+ class_key: validated.classKey,
1602
+ scrub_hits: validated.scrubHits,
1603
+ event: validated.event,
1604
+ };
1605
+ appendJournal(projectDir, eventRecord);
1606
+ // Emit the folded `replatform_run_event` row (spec 0007). No identity yet →
1607
+ // hold back; the finalize full re-push sends every folded class regardless.
1608
+ if (biSink.isGuid(state.dims.wix_user_id)) {
1609
+ await pushToBi(projectDir, ts, eventRecord.seq,
1610
+ [foldedRowForClass(state, eventRecord)], state.dims.wix_user_id, 'run_event');
1611
+ }
1612
+ const priorInClass = state.events.filter((r) => r.class_key === validated.classKey).length;
1613
+ return {
1614
+ recorded: true,
1615
+ seq: state.seq + 1,
1616
+ event_type: validated.event.event_type,
1617
+ folded_into_existing_class: priorInClass > 0,
1618
+ scrub_hits: validated.scrubHits,
1619
+ };
1620
+ }
1621
+
1622
+ function stage(projectDir, action, stageName, { outcome, now } = {}) {
1623
+ const ts = nowIso(now);
1624
+ const { state } = requireActiveRun(projectDir);
1625
+ const errors = [];
1626
+ checkEnum(errors, 'stage', stageName, STAGES);
1627
+ if (errors.length > 0) throw new ValidationError(errors);
1628
+ if (state.openWait) {
1629
+ throw new ValidationError(
1630
+ [`a wait interval is open (stage ${state.openWait.stage})`],
1631
+ 'call `wait end` before stage boundaries',
1632
+ );
1633
+ }
1634
+ if (action === 'start') {
1635
+ if (state.openStage) {
1636
+ throw new ValidationError(
1637
+ [`stage ${state.openStage} is still open`],
1638
+ `call \`stage end ${state.openStage} --outcome <passed|halted|failed|skipped>\` first`,
1639
+ );
1640
+ }
1641
+ appendJournal(projectDir, { type: 'stage_start', ts, seq: state.seq + 1, stage: stageName });
1642
+ return { stage: stageName, started: true };
1643
+ }
1644
+ if (action === 'end') {
1645
+ if (state.openStage !== stageName) {
1646
+ throw new ValidationError([
1647
+ state.openStage
1648
+ ? `open stage is ${state.openStage}, not ${stageName}`
1649
+ : `no stage is open (last stage: ${state.lastStage || 'none'})`,
1650
+ ]);
1651
+ }
1652
+ const resolvedOutcome = outcome === undefined || outcome === null ? 'passed' : outcome;
1653
+ const outcomeErrors = [];
1654
+ checkEnum(outcomeErrors, 'outcome', resolvedOutcome, STAGE_OUTCOMES);
1655
+ if (outcomeErrors.length > 0) throw new ValidationError(outcomeErrors);
1656
+ appendJournal(projectDir, { type: 'stage_end', ts, seq: state.seq + 1, stage: stageName, outcome: resolvedOutcome });
1657
+ return { stage: stageName, ended: true, outcome: resolvedOutcome };
1658
+ }
1659
+ throw new ValidationError([`stage action must be start or end (got: ${action})`]);
1660
+ }
1661
+
1662
+ // Attach measured latency/token counts to a stage. Separate from `stage end` on purpose: a stage may
1663
+ // be metered several times (each generated script reports its own invocation), and token counts are
1664
+ // known by the agent runtime rather than by whatever closed the stage.
1665
+ function meter(projectDir, input, { now } = {}) {
1666
+ const ts = nowIso(now);
1667
+ const { state } = requireActiveRun(projectDir);
1668
+ const payload = validateMeter(input, { openStage: state.openStage, lastStage: state.lastStage });
1669
+ appendJournal(projectDir, { type: 'meter', ts, seq: state.seq + 1, stage: payload.stage, meter: payload });
1670
+ const recorded = Object.keys(payload).filter((k) => k !== 'stage');
1671
+ return { metered: true, stage: payload.stage, fields: recorded };
1672
+ }
1673
+
1674
+ async function wait(projectDir, action, { now, haltSubtype, skill, what } = {}) {
1675
+ const ts = nowIso(now);
1676
+ const { state } = requireActiveRun(projectDir);
1677
+ if (action === 'start') {
1678
+ if (state.openWait) {
1679
+ throw new ValidationError(['a wait interval is already open']);
1680
+ }
1681
+ const attributedStage = state.openStage || state.lastStage || 'config';
1682
+ let seq = state.seq;
1683
+ let haltRecorded = false;
1684
+ if (haltSubtype !== undefined && haltSubtype !== null) {
1685
+ // The paired halt_needs_user event is emitted mechanically so a stalled
1686
+ // run never loses the "why" to a missed record call.
1687
+ const validated = validateEvent({
1688
+ event_type: 'halt_needs_user',
1689
+ subtype: haltSubtype,
1690
+ stage: attributedStage,
1691
+ skill: skill || 'replatform',
1692
+ severity: 'blocking',
1693
+ what_happened: what || `Run halted at ${attributedStage} waiting on the user (${haltSubtype}).`,
1694
+ }, state);
1695
+ seq += 1;
1696
+ const eventRecord = {
1697
+ type: 'event', ts, seq, class_key: validated.classKey, scrub_hits: validated.scrubHits, event: validated.event,
1698
+ };
1699
+ appendJournal(projectDir, eventRecord);
1700
+ if (biSink.isGuid(state.dims.wix_user_id)) {
1701
+ const pushed = await pushToBi(projectDir, ts, seq,
1702
+ [foldedRowForClass(state, eventRecord)], state.dims.wix_user_id, 'run_event');
1703
+ seq = pushed.seq;
1704
+ }
1705
+ haltRecorded = true;
1706
+ } else if (skill !== undefined || what !== undefined) {
1707
+ throw new ValidationError(['--skill/--what on wait start are only valid together with --halt <subtype>']);
1708
+ }
1709
+ seq += 1;
1710
+ appendJournal(projectDir, { type: 'wait_start', ts, seq, stage: attributedStage });
1711
+ return { waiting: true, stage: attributedStage, halt_recorded: haltRecorded };
1712
+ }
1713
+ if (action === 'end') {
1714
+ if (!state.openWait) {
1715
+ throw new ValidationError(['no wait interval is open']);
1716
+ }
1717
+ appendJournal(projectDir, { type: 'wait_end', ts, seq: state.seq + 1 });
1718
+ return { waiting: false, waited_ms: Math.max(0, tsMs(ts) - tsMs(state.openWait.start)) };
1719
+ }
1720
+ throw new ValidationError([`wait action must be start or end (got: ${action})`]);
1721
+ }
1722
+
1723
+ async function finalize(projectDir, rollupInput, { now } = {}) {
1724
+ const ts = nowIso(now);
1725
+ const { journal, state } = requireActiveRun(projectDir);
1726
+ const payload = validateFinalizeInput(rollupInput, state);
1727
+
1728
+ let seq = state.seq;
1729
+ if (state.openWait) {
1730
+ seq += 1;
1731
+ appendJournal(projectDir, { type: 'wait_end', ts, seq });
1732
+ }
1733
+ if (state.openStage) {
1734
+ seq += 1;
1735
+ appendJournal(projectDir, {
1736
+ type: 'stage_end',
1737
+ ts,
1738
+ seq,
1739
+ stage: state.openStage,
1740
+ outcome: OUTCOME_FOR_TERMINAL[payload.terminal_state],
1741
+ });
1742
+ }
1743
+ seq += 1;
1744
+ appendJournal(projectDir, { type: 'finalize', ts, seq, payload });
1745
+
1746
+ // BI push before archival, so a failed push is journaled into the run and
1747
+ // lands in the archived telemetry_health. The full re-push (started row +
1748
+ // every folded event class + the finalized rollup) heals any mid-run failure
1749
+ // or identity-not-yet-known hold-back: the stream is append-only and the
1750
+ // reviewer dedupes on the natural keys, latest row wins. The pushed rollup's
1751
+ // own health can't include this push's outcome (it is assembled first) — if
1752
+ // the push fails there is no BI row at all, and the local document, which is
1753
+ // authoritative, records the failure.
1754
+ let biPush = { sent: 0, failed: 0 };
1755
+ if (!biSink.disabled()) {
1756
+ const provisionalJournal = readJournal(projectDir);
1757
+ const provisional = assembleDocument(projectDir, provisionalJournal.records, provisionalJournal.malformed);
1758
+ const rollup = provisional.document.rollup;
1759
+ if (biSink.isGuid(rollup.wix_user_id)) {
1760
+ const rows = [
1761
+ biSink.startedRunRow(replay(provisionalJournal.records).runStart, rollup.wix_user_id),
1762
+ ...provisional.document.events.map(biSink.eventRow),
1763
+ biSink.finalizedRunRow(rollup),
1764
+ ];
1765
+ const pushed = await pushToBi(projectDir, ts, seq, rows, rollup.wix_user_id, 'finalize');
1766
+ seq = pushed.seq;
1767
+ biPush = { sent: pushed.sent, failed: pushed.failed };
1768
+ } else {
1769
+ // No operator identity by run end: the run is unroutable in BI. Journal
1770
+ // it as a push failure — this is real telemetry loss, not a hold-back.
1771
+ const rowCount = provisional.document.events.length + 2;
1772
+ seq += 1;
1773
+ appendJournal(projectDir, {
1774
+ type: 'bi_push_failed', ts, seq, target: 'finalize', rows: rowCount, detail: 'no_operator_identity',
1775
+ });
1776
+ biPush = { sent: 0, failed: rowCount };
1777
+ }
1778
+ }
1779
+
1780
+ const finalJournal = readJournal(projectDir);
1781
+ const finalState = replay(finalJournal.records);
1782
+ const assembled = archiveFinalizedJournal(projectDir, finalJournal, finalState);
1783
+
1784
+ const health = assembled.document.rollup.telemetry_health;
1785
+ return {
1786
+ finalized: true,
1787
+ run_id: finalState.runStart.run_id,
1788
+ attempt: finalState.runStart.attempt,
1789
+ terminal_state: payload.terminal_state,
1790
+ bi_push: biPush,
1791
+ signal_file: 'run-telemetry.json',
1792
+ event_classes: assembled.document.events.length,
1793
+ telemetry_health: health,
1794
+ evidence_bytes: assembled.evidenceBytes,
1795
+ ...(assembled.evidenceBytes > EVIDENCE_SIZE_FLAG_BYTES
1796
+ ? {
1797
+ evidence_breakdown: assembled.evidenceFiles
1798
+ .sort((a, b) => b.bytes - a.bytes)
1799
+ .slice(0, 10),
1800
+ }
1801
+ : {}),
1802
+ };
1803
+ }
1804
+
1805
+ async function rebuild(projectDir, { attempt, push } = {}) {
1806
+ // Re-assemble a finalized run's signal document from its archived journal.
1807
+ // The document is a pure function of the journal, so recorder fixes (fold
1808
+ // semantics, manifest rules, health flags) can be applied to past runs
1809
+ // without touching the captured record itself. With `push`, the rebuilt run
1810
+ // is re-emitted to BI (outage backfill / post-fix re-push) — idempotent by
1811
+ // the reviewer's query-time dedup on the natural keys.
1812
+ const dir = runsDir(projectDir);
1813
+ const archived = fs.existsSync(dir)
1814
+ ? fs.readdirSync(dir)
1815
+ .map((name) => {
1816
+ const match = /^run-(\d+)-.+\.jsonl$/.exec(name);
1817
+ return match ? { attempt: Number(match[1]), name } : null;
1818
+ })
1819
+ .filter(Boolean)
1820
+ : [];
1821
+ if (archived.length === 0) {
1822
+ throw new ValidationError(
1823
+ ['no archived run journals to rebuild from'],
1824
+ 'rebuild re-assembles a finalized signal document from telemetry/runs/run-<attempt>-<run_id>.jsonl',
1825
+ );
1826
+ }
1827
+ const latestAttempt = archived.reduce((max, a) => Math.max(max, a.attempt), 0);
1828
+ const target = attempt === undefined || attempt === null
1829
+ ? archived.find((a) => a.attempt === latestAttempt)
1830
+ : archived.find((a) => a.attempt === attempt);
1831
+ if (!target) {
1832
+ throw new ValidationError([`no archived journal for attempt ${attempt} (have: ${archived.map((a) => a.attempt).join(', ')})`]);
1833
+ }
1834
+ const journal = readJournalFile(path.join(dir, target.name));
1835
+ const state = replay(journal.records);
1836
+ if (!state.runStart || !state.finalize) {
1837
+ throw new ValidationError([`archived journal ${target.name} is not a finalized run`]);
1838
+ }
1839
+ const assembled = assembleDocument(projectDir, journal.records, journal.malformed);
1840
+ writeJsonAtomic(path.join(dir, `${archiveName(state)}.json`), assembled.document);
1841
+ const signalUpdated = target.attempt === latestAttempt;
1842
+ if (signalUpdated) {
1843
+ writeJsonAtomic(signalPath(projectDir), assembled.document);
1844
+ }
1845
+ // The archived journal is immutable, so a backfill push's outcome cannot be
1846
+ // journaled into the run — it is reported here, to the operator running the
1847
+ // explicit backfill, instead.
1848
+ let biPush;
1849
+ if (push) {
1850
+ const rollup = assembled.document.rollup;
1851
+ const rows = [
1852
+ biSink.startedRunRow(state.runStart, rollup.wix_user_id),
1853
+ ...assembled.document.events.map(biSink.eventRow),
1854
+ biSink.finalizedRunRow(rollup),
1855
+ ];
1856
+ const result = await biSink.pushRows(rows, rollup.wix_user_id);
1857
+ biPush = {
1858
+ sent: result.sent,
1859
+ failed: result.failures.reduce((sum, f) => sum + f.rows, 0),
1860
+ ...(result.failures.length > 0 ? { failure_details: [...new Set(result.failures.map((f) => f.detail))] } : {}),
1861
+ };
1862
+ }
1863
+ return {
1864
+ rebuilt: true,
1865
+ run_id: state.runStart.run_id,
1866
+ attempt: target.attempt,
1867
+ signal_file_updated: signalUpdated,
1868
+ event_classes: assembled.document.events.length,
1869
+ telemetry_health: assembled.document.rollup.telemetry_health,
1870
+ ...(biPush ? { bi_push: biPush } : {}),
1871
+ };
1872
+ }
1873
+
1874
+ function status(projectDir) {
1875
+ const journal = readJournal(projectDir);
1876
+ if (!journal) {
1877
+ const lastAttempt = lastFinalizedAttempt(projectDir);
1878
+ return { active: false, finalized_attempts: lastAttempt };
1879
+ }
1880
+ const state = replay(journal.records);
1881
+ return {
1882
+ active: !state.finalize,
1883
+ run_id: state.runStart ? state.runStart.run_id : null,
1884
+ attempt: state.runStart ? state.runStart.attempt : null,
1885
+ session_count: state.sessionCount,
1886
+ open_stage: state.openStage,
1887
+ open_wait: Boolean(state.openWait),
1888
+ events_recorded: state.events.length,
1889
+ distinct_classes: new Set(state.events.map((r) => r.class_key)).size,
1890
+ rejected_calls: state.rejected.length,
1891
+ last_seq: state.seq,
1892
+ };
1893
+ }
1894
+
1895
+ module.exports = {
1896
+ TELEMETRY_SCHEMA_VERSION,
1897
+ STAGES,
1898
+ STAGE_OUTCOMES,
1899
+ TERMINAL_STATES,
1900
+ SEVERITIES,
1901
+ EVENT_SUBTYPES,
1902
+ FIELD_TIERS,
1903
+ FREE_TEXT_MAX,
1904
+ SHAPES_KEPT_PER_CLASS,
1905
+ ValidationError,
1906
+ scrubText,
1907
+ validateDims,
1908
+ validateEvent,
1909
+ validateFinalizeInput,
1910
+ journalPath,
1911
+ signalPath,
1912
+ runsDir,
1913
+ readJournal,
1914
+ start,
1915
+ dims,
1916
+ record,
1917
+ stage,
1918
+ meter,
1919
+ wait,
1920
+ finalize,
1921
+ rebuild,
1922
+ status,
1923
+ validateMeter,
1924
+ computeCost,
1925
+ METER_DURATION_KEYS,
1926
+ METER_COUNT_KEYS,
1927
+ };