@pixel-point/toolcraft 0.0.18 → 0.0.19

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 (384) hide show
  1. package/package.json +1 -1
  2. package/src/generate-demand-only-lifecycle.test.mjs +9 -1
  3. package/src/generate-performance-iteration-execution.test.mjs +3 -3
  4. package/src/generate-test-delivery-architecture-assertions.mjs +11 -73
  5. package/src/generate-test-demand-only-lifecycle.mjs +34 -36
  6. package/src/generate-test-dependency-sandbox.mjs +4 -1
  7. package/src/generate-test-doc-assertions.mjs +55 -53
  8. package/src/generate-test-doc-assertions.test.mjs +1 -1
  9. package/src/generate-test-doc-policy-assertions.mjs +1 -1
  10. package/src/generate-test-export-doc-assertions.mjs +53 -0
  11. package/src/generate-test-file-assertions.mjs +51 -8
  12. package/src/generate-test-integrity-assertions.mjs +5 -2
  13. package/src/generate-test-integrity-manifest-assertions.mjs +16 -5
  14. package/src/generate-test-integrity-ownership-assertions.mjs +36 -4
  15. package/src/generate-test-package-assertions.mjs +4 -0
  16. package/src/generate-test-product-fixture.mjs +1 -1
  17. package/src/generate-test-product-fixture.test.mjs +35 -38
  18. package/src/generate-test-product-iteration.mjs +0 -1
  19. package/src/generate-test-product-iteration.test.mjs +0 -1
  20. package/src/generate-test-runtime-boundary-assertions.mjs +20 -0
  21. package/src/generate-test-standalone-assertions.mjs +0 -16
  22. package/src/generate-test-standalone-delivery-stages.mjs +2 -86
  23. package/src/generate.test.mjs +4 -0
  24. package/src/generated-integrity-manifest.test.mjs +5 -2
  25. package/src/generated-source-policy.test.mjs +0 -4
  26. package/src/package-json.mjs +5 -0
  27. package/src/package-json.test.mjs +21 -0
  28. package/src/package-manager.mjs +2 -0
  29. package/src/package-manager.test.mjs +8 -0
  30. package/src/packaged-template-fallback-helpers.mjs +254 -0
  31. package/src/packaged-template-fallback.test.mjs +121 -46
  32. package/src/packaged-template-runtime-assertions.mjs +150 -0
  33. package/templates/runtime/contracts/component-contracts.choices.test.ts +18 -3
  34. package/templates/runtime/contracts/component-contracts.choices.ts +8 -3
  35. package/templates/runtime/contracts/component-contracts.inputs.ts +2 -1
  36. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +18 -4
  37. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +1 -1
  38. package/templates/runtime/contracts/component-contracts.runtime.ts +7 -4
  39. package/templates/runtime/contracts/component-contracts.visual.test.ts +20 -0
  40. package/templates/runtime/contracts/component-contracts.visual.ts +4 -0
  41. package/templates/runtime/contracts/decision-contracts.test.ts +18 -12
  42. package/templates/runtime/contracts/decision-contracts.ts +4 -4
  43. package/templates/runtime/contracts/performance-verification-policy.test.ts +12 -12
  44. package/templates/runtime/contracts/performance-verification-policy.ts +8 -8
  45. package/templates/runtime/export/artifact-download.ts +1 -0
  46. package/templates/runtime/export/artifact-frame-renderer.ts +2 -15
  47. package/templates/runtime/export/artifact-frame-state.ts +16 -0
  48. package/templates/runtime/export/artifact-scene-frame.test.ts +3 -3
  49. package/templates/runtime/export/artifact-scene-frame.ts +3 -3
  50. package/templates/runtime/export/export-error.ts +5 -0
  51. package/templates/runtime/export/export-renderer-coverage.ts +54 -0
  52. package/templates/runtime/export/image-artifact-export.ts +2 -2
  53. package/templates/runtime/export/index.ts +13 -0
  54. package/templates/runtime/export/product-export-renderer.test.ts +38 -4
  55. package/templates/runtime/export/product-export-renderer.ts +0 -41
  56. package/templates/runtime/export/product-svg-export-renderer.ts +21 -0
  57. package/templates/runtime/export/svg-artifact-export.test.ts +98 -0
  58. package/templates/runtime/export/svg-artifact-export.ts +120 -0
  59. package/templates/runtime/export/svg-document.test.ts +221 -0
  60. package/templates/runtime/export/svg-document.ts +400 -0
  61. package/templates/runtime/export/svg-policy.ts +47 -0
  62. package/templates/runtime/index.ts +1 -0
  63. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +10 -0
  64. package/templates/runtime/react/app-shell/settings-transfer.test.ts +21 -0
  65. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +46 -1
  66. package/templates/runtime/react/app-shell/toolcraft-app.tsx +7 -2
  67. package/templates/runtime/react/canvas/canvas-shell-interactions.test.tsx +30 -0
  68. package/templates/runtime/react/canvas/canvas-shell-pinch-interactions.test.tsx +155 -0
  69. package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +1 -1
  70. package/templates/runtime/react/canvas/canvas-shell.tsx +16 -5
  71. package/templates/runtime/react/canvas/canvas-viewport-geometry.ts +133 -0
  72. package/templates/runtime/react/canvas/use-canvas-viewport-interactions.ts +285 -36
  73. package/templates/runtime/react/controls-panel/__tests__/controls-panel.color-applicability-layout.test.tsx +134 -0
  74. package/templates/runtime/react/controls-panel/__tests__/controls-panel.color-layout.test.tsx +16 -1
  75. package/templates/runtime/react/controls-panel/__tests__/controls-panel.render-isolation.test.tsx +2 -0
  76. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +39 -0
  77. package/templates/runtime/react/controls-panel/__tests__/controls-panel.setup-controls.test.tsx +3 -3
  78. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-collection.test.tsx +1 -4
  79. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-color-bank.test.tsx +2 -2
  80. package/templates/runtime/react/controls-panel/__tests__/controls-panel.vector-interaction.test.tsx +6 -6
  81. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +2 -2
  82. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +40 -11
  83. package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +31 -2
  84. package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +4 -1
  85. package/templates/runtime/react/controls-panel/renderers/controls-panel-action-renderer.tsx +2 -1
  86. package/templates/runtime/react/controls-panel/renderers/controls-panel-basic-renderers.tsx +2 -52
  87. package/templates/runtime/react/controls-panel/values/controls-panel-color-values.ts +1 -9
  88. package/templates/runtime/react/controls-panel/values/controls-panel-spatial-values.ts +7 -2
  89. package/templates/runtime/react/orientation-gizmo/orientation-gizmo-math.ts +9 -69
  90. package/templates/runtime/schema/artifact-export-actions.test.ts +47 -0
  91. package/templates/runtime/schema/artifact-export-actions.ts +49 -0
  92. package/templates/runtime/schema/types.ts +1 -0
  93. package/templates/runtime/state/canvas-background-state.ts +1 -16
  94. package/templates/runtime/state/canvas-state.ts +32 -20
  95. package/templates/runtime/state/control-value-codecs.test.ts +98 -0
  96. package/templates/runtime/state/control-value-codecs.ts +700 -0
  97. package/templates/runtime/state/control-value-normalization.test.ts +1 -1
  98. package/templates/runtime/state/control-value-normalization.ts +127 -135
  99. package/templates/runtime/state/controls-reducer.ts +26 -3
  100. package/templates/runtime/state/create-template-state.test.ts +30 -0
  101. package/templates/runtime/state/create-template-state.ts +52 -31
  102. package/templates/runtime/state/orientation-pose.ts +80 -0
  103. package/templates/runtime/state/persistence-control-values.test.ts +59 -0
  104. package/templates/runtime/state/reducer-controls.test.ts +45 -0
  105. package/templates/runtime/state/reducer-media-reset.test.ts +1 -1
  106. package/templates/runtime/state/reducer-timeline.test.ts +70 -0
  107. package/templates/runtime/state/timeline-reducer.ts +40 -2
  108. package/templates/runtime/testing/performance-schema-queries.ts +2 -2
  109. package/templates/starter/AGENTS.md +21 -22
  110. package/templates/starter/docs/toolcraft/README.md +6 -6
  111. package/templates/starter/docs/toolcraft/acceptance-testing.md +20 -20
  112. package/templates/starter/docs/toolcraft/agent-worklog.md +88 -1
  113. package/templates/starter/docs/toolcraft/assembly-workflow.md +7 -6
  114. package/templates/starter/docs/toolcraft/component-rules.md +2 -2
  115. package/templates/starter/docs/toolcraft/core/layout.md +6 -1
  116. package/templates/starter/docs/toolcraft/core/media-upload.md +1 -1
  117. package/templates/starter/docs/toolcraft/core/performance.md +8 -10
  118. package/templates/starter/docs/toolcraft/core/reference-study.md +80 -19
  119. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +2 -1
  120. package/templates/starter/docs/toolcraft/core/setup-export.md +43 -13
  121. package/templates/starter/docs/toolcraft/decision-contract.md +2 -2
  122. package/templates/starter/docs/toolcraft/performance.md +8 -10
  123. package/templates/starter/docs/toolcraft/renderer-technique.md +2 -2
  124. package/templates/starter/docs/toolcraft/schema-reference.md +8 -8
  125. package/templates/starter/docs/toolcraft/workflow.md +14 -14
  126. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +38 -0
  127. package/templates/starter/e2e/app-browser-motion-reference-requirements.spec.ts +411 -0
  128. package/templates/starter/e2e/app-browser-proof-session.spec.ts +85 -0
  129. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +62 -0
  130. package/templates/starter/e2e/app-browser-selection-scope-evidence.spec.ts +137 -0
  131. package/templates/starter/e2e/browser-infinity-canvas-svg-evidence.ts +42 -0
  132. package/templates/starter/e2e/browser-layer-evidence-helpers.ts +15 -36
  133. package/templates/starter/e2e/browser-product-raster-snapshot.ts +213 -0
  134. package/templates/starter/e2e/browser-proof-session.ts +43 -15
  135. package/templates/starter/e2e/browser-proof-surface-action.ts +79 -0
  136. package/templates/starter/e2e/browser-proof-types.ts +15 -0
  137. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +22 -6
  138. package/templates/starter/e2e/browser-selection-scope-evidence.ts +269 -0
  139. package/templates/starter/e2e/browser-semantic-evidence-test-helpers.ts +97 -0
  140. package/templates/starter/e2e/browser-svg-export-evidence.spec.ts +79 -0
  141. package/templates/starter/e2e/browser-svg-export-evidence.ts +152 -0
  142. package/templates/starter/e2e/export-artifact-helpers.ts +70 -0
  143. package/templates/starter/e2e/product-observable-helpers.ts +8 -129
  144. package/templates/starter/e2e/svg-artifact-inspection.spec.ts +136 -0
  145. package/templates/starter/e2e/svg-artifact-inspection.ts +331 -0
  146. package/templates/starter/e2e/svg-download-test-fixtures.ts +34 -0
  147. package/templates/starter/e2e/toolcraft-delivery-catalog-reporter.ts +198 -15
  148. package/templates/starter/e2e/toolcraft-feature-verification-reporter.spec.ts +149 -0
  149. package/templates/starter/e2e/toolcraft-feature-verification-reporter.ts +223 -0
  150. package/templates/starter/package.json +2 -0
  151. package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
  152. package/templates/starter/scripts/create-toolcraft-motion-reference-study.mjs +233 -0
  153. package/templates/starter/scripts/create-toolcraft-motion-reference-study.test.mjs +459 -0
  154. package/templates/starter/scripts/playwright-test-title-selection.d.mts +45 -0
  155. package/templates/starter/scripts/run-browser-performance-test-helpers.mjs +0 -27
  156. package/templates/starter/scripts/run-browser-performance.mjs +0 -6
  157. package/templates/starter/scripts/run-browser-performance.test.mjs +5 -13
  158. package/templates/starter/scripts/run-delivery-verification-test-helpers.mjs +0 -34
  159. package/templates/starter/scripts/run-delivery-verification.test.mjs +135 -31
  160. package/templates/starter/scripts/run-feature-verification.mjs +212 -0
  161. package/templates/starter/scripts/run-feature-verification.test.mjs +226 -0
  162. package/templates/starter/scripts/toolcraft-checkpoint-delivery-state.mjs +21 -8
  163. package/templates/starter/scripts/toolcraft-checkpoint-delivery-state.test.mjs +32 -92
  164. package/templates/starter/scripts/toolcraft-checkpoint-transaction.mjs +28 -6
  165. package/templates/starter/scripts/toolcraft-checkpoint-transaction.test.mjs +8 -63
  166. package/templates/starter/scripts/toolcraft-delivery-anchor.mjs +28 -8
  167. package/templates/starter/scripts/toolcraft-delivery-anchor.test.mjs +50 -61
  168. package/templates/starter/scripts/toolcraft-delivery-architecture-inventory.mjs +0 -4
  169. package/templates/starter/scripts/toolcraft-delivery-architecture-policy.mjs +0 -1
  170. package/templates/starter/scripts/toolcraft-delivery-evidence.test.mjs +2 -3
  171. package/templates/starter/scripts/toolcraft-delivery-executor.mjs +5 -1
  172. package/templates/starter/scripts/toolcraft-delivery-executor.test.mjs +5 -12
  173. package/templates/starter/scripts/toolcraft-delivery-functional-context.d.mts +1 -48
  174. package/templates/starter/scripts/toolcraft-delivery-functional-context.mjs +2 -30
  175. package/templates/starter/scripts/toolcraft-delivery-functional-context.test.mjs +4 -166
  176. package/templates/starter/scripts/toolcraft-delivery-functional-initial.test.mjs +4 -20
  177. package/templates/starter/scripts/toolcraft-delivery-lifecycle.mjs +43 -192
  178. package/templates/starter/scripts/toolcraft-delivery-performance-iteration-lifecycle.test.mjs +79 -440
  179. package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +102 -297
  180. package/templates/starter/scripts/toolcraft-delivery-plan-construction.mjs +46 -97
  181. package/templates/starter/scripts/toolcraft-delivery-plan-test-helpers.mjs +125 -0
  182. package/templates/starter/scripts/toolcraft-delivery-plan-validation.test.mjs +58 -0
  183. package/templates/starter/scripts/toolcraft-delivery-plan.d.mts +19 -31
  184. package/templates/starter/scripts/toolcraft-delivery-plan.mjs +254 -252
  185. package/templates/starter/scripts/toolcraft-delivery-plan.test.mjs +29 -457
  186. package/templates/starter/scripts/toolcraft-delivery-receipt-performance.test.mjs +1 -1
  187. package/templates/starter/scripts/toolcraft-delivery-receipt-strictness.test.mjs +3 -72
  188. package/templates/starter/scripts/toolcraft-delivery-receipt-test-helpers.mjs +7 -36
  189. package/templates/starter/scripts/toolcraft-delivery-receipt.d.mts +3 -5
  190. package/templates/starter/scripts/toolcraft-delivery-receipt.mjs +15 -31
  191. package/templates/starter/scripts/toolcraft-delivery-receipt.test.mjs +28 -20
  192. package/templates/starter/scripts/toolcraft-delivery-receipt.types.test.mts +22 -30
  193. package/templates/starter/scripts/toolcraft-feature-verification-plan.d.mts +20 -0
  194. package/templates/starter/scripts/toolcraft-feature-verification-plan.mjs +146 -0
  195. package/templates/starter/scripts/toolcraft-feature-verification-plan.test.mjs +96 -0
  196. package/templates/starter/scripts/toolcraft-functional-proof-model.d.mts +3 -18
  197. package/templates/starter/scripts/toolcraft-functional-proof-model.mjs +39 -127
  198. package/templates/starter/scripts/toolcraft-functional-proof-model.test.mjs +25 -362
  199. package/templates/starter/scripts/toolcraft-functional-proof-model.types.test.mts +6 -83
  200. package/templates/starter/scripts/toolcraft-functional-proof-primitives.d.mts +11 -0
  201. package/templates/starter/scripts/toolcraft-functional-proof-primitives.mjs +61 -0
  202. package/templates/starter/scripts/toolcraft-functional-proof-primitives.test.mjs +43 -0
  203. package/templates/starter/scripts/toolcraft-integrity-policy.mjs +13 -0
  204. package/templates/starter/scripts/toolcraft-integrity-test-utils.mjs +1 -1
  205. package/templates/starter/scripts/toolcraft-layer-evidence-facade.test.mjs +58 -22
  206. package/templates/starter/scripts/toolcraft-motion-reference-analysis-protocol.mjs +111 -0
  207. package/templates/starter/scripts/toolcraft-motion-reference-analysis-protocol.test.mjs +116 -0
  208. package/templates/starter/scripts/toolcraft-motion-reference-artifacts.d.mts +37 -0
  209. package/templates/starter/scripts/toolcraft-motion-reference-artifacts.mjs +308 -0
  210. package/templates/starter/scripts/toolcraft-motion-reference-artifacts.test.mjs +420 -0
  211. package/templates/starter/scripts/toolcraft-motion-reference-artifacts.types.test.mts +50 -0
  212. package/templates/starter/scripts/toolcraft-motion-reference-command-options.mjs +195 -0
  213. package/templates/starter/scripts/toolcraft-motion-reference-command-options.test.mjs +224 -0
  214. package/templates/starter/scripts/toolcraft-motion-reference-contact-sheet.mjs +122 -0
  215. package/templates/starter/scripts/toolcraft-motion-reference-contact-sheet.test.mjs +153 -0
  216. package/templates/starter/scripts/toolcraft-motion-reference-evidence-assembly.mjs +270 -0
  217. package/templates/starter/scripts/toolcraft-motion-reference-evidence.test.mjs +432 -0
  218. package/templates/starter/scripts/toolcraft-motion-reference-ffmpeg-output.mjs +275 -0
  219. package/templates/starter/scripts/toolcraft-motion-reference-ffmpeg-output.test.mjs +294 -0
  220. package/templates/starter/scripts/toolcraft-motion-reference-group-inventory.mjs +148 -0
  221. package/templates/starter/scripts/toolcraft-motion-reference-image-filter-graph.mjs +101 -0
  222. package/templates/starter/scripts/toolcraft-motion-reference-image-filter-graph.test.mjs +124 -0
  223. package/templates/starter/scripts/toolcraft-motion-reference-image-source.mjs +259 -0
  224. package/templates/starter/scripts/toolcraft-motion-reference-image-source.test.mjs +338 -0
  225. package/templates/starter/scripts/toolcraft-motion-reference-image-timing.mjs +96 -0
  226. package/templates/starter/scripts/toolcraft-motion-reference-image-timing.test.mjs +81 -0
  227. package/templates/starter/scripts/toolcraft-motion-reference-lock-recovery.test.mjs +144 -0
  228. package/templates/starter/scripts/toolcraft-motion-reference-media-source.mjs +165 -0
  229. package/templates/starter/scripts/toolcraft-motion-reference-media-source.test.mjs +299 -0
  230. package/templates/starter/scripts/toolcraft-motion-reference-publication-discovery.test.mjs +142 -0
  231. package/templates/starter/scripts/toolcraft-motion-reference-publication-isolation.test.mjs +234 -0
  232. package/templates/starter/scripts/toolcraft-motion-reference-publication-journal-schema.mjs +122 -0
  233. package/templates/starter/scripts/toolcraft-motion-reference-publication-journal-store.mjs +89 -0
  234. package/templates/starter/scripts/toolcraft-motion-reference-publication-journal-store.test.mjs +74 -0
  235. package/templates/starter/scripts/toolcraft-motion-reference-publication-journal.mjs +264 -0
  236. package/templates/starter/scripts/toolcraft-motion-reference-publication-journal.test.mjs +377 -0
  237. package/templates/starter/scripts/toolcraft-motion-reference-publication-lease.mjs +254 -0
  238. package/templates/starter/scripts/toolcraft-motion-reference-publication-lease.test.mjs +130 -0
  239. package/templates/starter/scripts/toolcraft-motion-reference-publication-staging.mjs +128 -0
  240. package/templates/starter/scripts/toolcraft-motion-reference-publication-staging.test.mjs +209 -0
  241. package/templates/starter/scripts/toolcraft-motion-reference-publication.fixtures.mjs +88 -0
  242. package/templates/starter/scripts/toolcraft-motion-reference-publication.mjs +249 -0
  243. package/templates/starter/scripts/toolcraft-motion-reference-publication.test.mjs +433 -0
  244. package/templates/starter/scripts/toolcraft-motion-reference-review-output.mjs +38 -0
  245. package/templates/starter/scripts/toolcraft-motion-reference-review-output.test.mjs +29 -0
  246. package/templates/starter/scripts/toolcraft-motion-reference-root-safety.test.mjs +84 -0
  247. package/templates/starter/scripts/toolcraft-motion-reference-roots.mjs +83 -0
  248. package/templates/starter/scripts/toolcraft-motion-reference-sampling.mjs +283 -0
  249. package/templates/starter/scripts/toolcraft-motion-reference-sampling.test.mjs +480 -0
  250. package/templates/starter/scripts/toolcraft-motion-reference-stage-registry.mjs +39 -0
  251. package/templates/starter/scripts/toolcraft-motion-reference-stage-registry.test.mjs +59 -0
  252. package/templates/starter/scripts/toolcraft-motion-reference-stage.mjs +71 -0
  253. package/templates/starter/scripts/toolcraft-motion-reference-stage.test.mjs +61 -0
  254. package/templates/starter/scripts/toolcraft-motion-review-numeric.mjs +74 -0
  255. package/templates/starter/scripts/toolcraft-motion-review-numeric.test.mjs +65 -0
  256. package/templates/starter/scripts/toolcraft-motion-review-partitioning.mjs +268 -0
  257. package/templates/starter/scripts/toolcraft-motion-review-partitioning.test.mjs +329 -0
  258. package/templates/starter/scripts/toolcraft-motion-review-protected-units.mjs +120 -0
  259. package/templates/starter/scripts/toolcraft-motion-review-protected-units.test.mjs +111 -0
  260. package/templates/starter/scripts/toolcraft-performance-escalation-policy.test.mjs +77 -345
  261. package/templates/starter/scripts/toolcraft-performance-receipt-validation.test.mjs +19 -21
  262. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +1 -1
  263. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +10 -1
  264. package/templates/starter/scripts/toolcraft-product-export-boundary.test.mjs +5 -1
  265. package/templates/starter/scripts/toolcraft-product-observable-facade.test.mjs +46 -1
  266. package/templates/starter/scripts/toolcraft-receipt-boundary.test.mjs +0 -1
  267. package/templates/starter/scripts/toolcraft-source-inventory.d.mts +5 -0
  268. package/templates/starter/scripts/toolcraft-source-inventory.mjs +25 -14
  269. package/templates/starter/scripts/toolcraft-source-inventory.test.mjs +48 -0
  270. package/templates/starter/scripts/toolcraft-source-ownership.mjs +0 -1
  271. package/templates/starter/scripts/toolcraft-source-ownership.test.mjs +30 -5
  272. package/templates/starter/scripts/toolcraft-verification-receipt-test-helpers.mjs +5 -26
  273. package/templates/starter/scripts/toolcraft-verification-receipt.d.mts +0 -7
  274. package/templates/starter/scripts/toolcraft-verification-receipt.mjs +3 -47
  275. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +6 -87
  276. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.d.mts +19 -0
  277. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.mjs +107 -0
  278. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.test.mjs +114 -0
  279. package/templates/starter/scripts/toolcraft-vitest-runtime-evidence-reporter.mjs +13 -2
  280. package/templates/starter/scripts/toolcraft-vitest-runtime-evidence-reporter.test.mjs +110 -0
  281. package/templates/starter/src/app/acceptance/artifact-export-intent.ts +51 -20
  282. package/templates/starter/src/app/acceptance/canvas-coverage.ts +1 -0
  283. package/templates/starter/src/app/acceptance/color-bank-labels.ts +11 -15
  284. package/templates/starter/src/app/acceptance/color-row-grouping.ts +33 -0
  285. package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +0 -11
  286. package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +2 -3
  287. package/templates/starter/src/app/acceptance/control-component-rules.ts +0 -10
  288. package/templates/starter/src/app/acceptance/control-labels.ts +30 -1
  289. package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +18 -0
  290. package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +25 -17
  291. package/templates/starter/src/app/acceptance/feature-verification-selection.test.ts +295 -0
  292. package/templates/starter/src/app/acceptance/feature-verification-selection.ts +161 -0
  293. package/templates/starter/src/app/acceptance/infinity-canvas.ts +10 -0
  294. package/templates/starter/src/app/acceptance/interaction-ownership-intent.ts +19 -1
  295. package/templates/starter/src/app/acceptance/interaction-ownership.ts +18 -2
  296. package/templates/starter/src/app/acceptance/motion-reference-evidence-validation.mjs +65 -0
  297. package/templates/starter/src/app/acceptance/motion-reference-evidence-windows.mjs +301 -0
  298. package/templates/starter/src/app/acceptance/motion-reference-evidence.d.mts +11 -0
  299. package/templates/starter/src/app/acceptance/motion-reference-evidence.mjs +145 -0
  300. package/templates/starter/src/app/acceptance/motion-reference-study-behaviors.ts +162 -0
  301. package/templates/starter/src/app/acceptance/motion-reference-study-coverage.ts +206 -0
  302. package/templates/starter/src/app/acceptance/motion-reference-study-identity.ts +129 -0
  303. package/templates/starter/src/app/acceptance/motion-reference-study-partitions.ts +186 -0
  304. package/templates/starter/src/app/acceptance/motion-reference-study.ts +90 -0
  305. package/templates/starter/src/app/acceptance/output-export-actions.test.ts +9 -1
  306. package/templates/starter/src/app/acceptance/output-export-actions.ts +14 -22
  307. package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +5 -1
  308. package/templates/starter/src/app/acceptance/output-export-model.ts +4 -0
  309. package/templates/starter/src/app/acceptance/output-export.ts +4 -1
  310. package/templates/starter/src/app/acceptance/reference-study-types.ts +167 -17
  311. package/templates/starter/src/app/acceptance/runtime-coverage.ts +6 -1
  312. package/templates/starter/src/app/acceptance/selection-scope.test.ts +200 -0
  313. package/templates/starter/src/app/acceptance/selection-scope.ts +151 -0
  314. package/templates/starter/src/app/acceptance/transfer-mode-types.ts +114 -0
  315. package/templates/starter/src/app/acceptance/types.ts +43 -118
  316. package/templates/starter/src/app/acceptance/validate-coverage.ts +14 -3
  317. package/templates/starter/src/app/starter-acceptance-data.ts +1 -0
  318. package/templates/starter/src/app/starter-acceptance.animation-intent.test.ts +3 -0
  319. package/templates/starter/src/app/starter-acceptance.artifact-export-intent.test.ts +81 -8
  320. package/templates/starter/src/app/starter-acceptance.background-export.test.ts +2 -0
  321. package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +2 -0
  322. package/templates/starter/src/app/starter-acceptance.color-row-grouping.test.ts +118 -0
  323. package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +1 -1
  324. package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +1 -0
  325. package/templates/starter/src/app/starter-acceptance.control-applicability.test.ts +1 -0
  326. package/templates/starter/src/app/starter-acceptance.control-label-context.test.ts +1 -0
  327. package/templates/starter/src/app/starter-acceptance.control-labels.test.ts +78 -3
  328. package/templates/starter/src/app/starter-acceptance.control-naming-rules.test.ts +77 -1
  329. package/templates/starter/src/app/starter-acceptance.custom-control-fit-check.test.ts +1 -1
  330. package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +74 -3
  331. package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +5 -1
  332. package/templates/starter/src/app/starter-acceptance.image-export-settings.test.ts +2 -0
  333. package/templates/starter/src/app/starter-acceptance.interaction-ownership.test.ts +19 -3
  334. package/templates/starter/src/app/starter-acceptance.layers.test.ts +2 -1
  335. package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +28 -0
  336. package/templates/starter/src/app/starter-acceptance.motion-reference-evidence.test.ts +17 -0
  337. package/templates/starter/src/app/starter-acceptance.motion-reference-identity.test.ts +163 -0
  338. package/templates/starter/src/app/starter-acceptance.motion-reference-mapping.test.ts +133 -0
  339. package/templates/starter/src/app/starter-acceptance.motion-reference-ownership.test.ts +121 -0
  340. package/templates/starter/src/app/starter-acceptance.motion-reference-partitions.test.ts +210 -0
  341. package/templates/starter/src/app/starter-acceptance.motion-reference-test-utils.ts +313 -0
  342. package/templates/starter/src/app/starter-acceptance.output-export-intent.test.ts +78 -4
  343. package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +6 -1
  344. package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +18 -1
  345. package/templates/starter/src/app/starter-acceptance.product-worklog.test.ts +123 -2
  346. package/templates/starter/src/app/starter-acceptance.reference-custom-timeline.test.ts +2 -0
  347. package/templates/starter/src/app/starter-acceptance.reference-foundation.test.ts +8 -0
  348. package/templates/starter/src/app/starter-acceptance.reference-timeline.test.ts +7 -0
  349. package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +2 -0
  350. package/templates/starter/src/app/starter-acceptance.timeline-playback.test.ts +5 -0
  351. package/templates/starter/src/app/starter-acceptance.ts +34 -8
  352. package/templates/starter/src/app/starter-acceptance.video-export-settings.test.ts +2 -0
  353. package/templates/starter/src/app/starter-acceptance.video-reference-study.test.ts +34 -236
  354. package/templates/starter/src/app/starter-acceptance.view-interaction.test.ts +4 -0
  355. package/templates/starter/src/app/starter-acceptance.worklog-test-utils.ts +122 -6
  356. package/templates/starter/src/app/starter-acceptance.worklog.test.ts +152 -16
  357. package/templates/starter/src/app/starter-performance.gates.test.ts +20 -18
  358. package/templates/starter/src/app/starter-performance.lifecycle.test.ts +22 -6
  359. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +3 -0
  360. package/templates/starter/tsconfig.json +1 -0
  361. package/templates/starter/scripts/toolcraft-browser-impact-selection.mjs +0 -26
  362. package/templates/starter/scripts/toolcraft-delivery-framework-refresh.test.mjs +0 -141
  363. package/templates/starter/scripts/toolcraft-delivery-functional-changed.test.mjs +0 -468
  364. package/templates/starter/scripts/toolcraft-delivery-functional-model-lifecycle.test.mjs +0 -254
  365. package/templates/starter/scripts/toolcraft-delivery-functional-semantic-regression.test.mjs +0 -135
  366. package/templates/starter/scripts/toolcraft-delivery-performance-iteration-durability.test.mjs +0 -329
  367. package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +0 -430
  368. package/templates/starter/scripts/toolcraft-delivery-plan-functional-model.test.mjs +0 -358
  369. package/templates/starter/scripts/toolcraft-functional-proof-regression-fixtures.mjs +0 -298
  370. package/templates/starter/scripts/toolcraft-verification-impact-inventory.d.mts +0 -62
  371. package/templates/starter/scripts/toolcraft-verification-impact-inventory.mjs +0 -344
  372. package/templates/starter/scripts/toolcraft-verification-impact-inventory.test.mjs +0 -204
  373. package/templates/starter/scripts/toolcraft-verification-impact-performance-candidates.test.mjs +0 -210
  374. package/templates/starter/scripts/toolcraft-verification-impact-resolution.mjs +0 -344
  375. package/templates/starter/scripts/toolcraft-verification-impact-resolution.test.mjs +0 -270
  376. package/templates/starter/scripts/toolcraft-verification-impact-semantic-resolution.test.mjs +0 -449
  377. package/templates/starter/scripts/toolcraft-verification-impact-support-resolution.test.mjs +0 -195
  378. package/templates/starter/scripts/toolcraft-verification-impact.d.mts +0 -132
  379. package/templates/starter/scripts/toolcraft-verification-impact.mjs +0 -169
  380. package/templates/starter/scripts/toolcraft-verification-impact.test.mjs +0 -300
  381. package/templates/starter/scripts/toolcraft-verification-input-roles.mjs +0 -197
  382. package/templates/starter/scripts/toolcraft-verification-input-roles.test.mjs +0 -225
  383. package/templates/starter/src/app/acceptance/video-reference-study.ts +0 -217
  384. package/templates/starter/src/app/starter-verification-impact.json +0 -15
@@ -0,0 +1,254 @@
1
+ import path from "node:path";
2
+
3
+ function fail(message, cause) {
4
+ throw new Error(`Toolcraft motion reference publication failed: ${message}`, { cause });
5
+ }
6
+
7
+ function existsError(error) {
8
+ return [error?.code, error?.cause?.code].some((code) =>
9
+ code === "EEXIST" || code === "ENOTEMPTY");
10
+ }
11
+
12
+ function validateOptions(options) {
13
+ if (!path.isAbsolute(options.lockDirectory ?? "") ||
14
+ typeof options.sourceIdentity !== "string" || !options.sourceIdentity ||
15
+ !Number.isSafeInteger(options.pid) || options.pid <= 0 ||
16
+ typeof options.now !== "function" || typeof options.tokenFactory !== "function" ||
17
+ !Number.isFinite(options.lockLeaseMilliseconds) ||
18
+ options.lockLeaseMilliseconds <= 0 ||
19
+ !Number.isFinite(options.maximumUninterruptibleOperationMilliseconds) ||
20
+ options.maximumUninterruptibleOperationMilliseconds <= 0 ||
21
+ options.maximumUninterruptibleOperationMilliseconds * 2 >
22
+ options.lockLeaseMilliseconds) {
23
+ fail("lock identity, clock, lease, operation bound, PID, and token factory are required");
24
+ }
25
+ }
26
+
27
+ function validateOwner(owner) {
28
+ if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 ||
29
+ !Number.isFinite(owner.leaseExpiresAt) ||
30
+ typeof owner.sourceIdentity !== "string" || !owner.sourceIdentity ||
31
+ typeof owner.token !== "string" || !owner.token) {
32
+ fail("existing lock metadata is invalid and must be inspected manually");
33
+ }
34
+ return owner;
35
+ }
36
+
37
+ async function readOwnerFile(fileSystem, ownerPath) {
38
+ try {
39
+ return validateOwner(JSON.parse(await fileSystem.readFile(ownerPath, "utf8")));
40
+ } catch (error) {
41
+ if (error.message?.startsWith("Toolcraft motion reference publication failed:")) {
42
+ throw error;
43
+ }
44
+ fail("existing lock metadata could not be read", error);
45
+ }
46
+ }
47
+
48
+ const ownerPath = (lockDirectory) => path.join(lockDirectory, "owner.json");
49
+ const claimPath = (lockDirectory) => path.join(lockDirectory, "reclaim.json");
50
+
51
+ async function readOwner(options) {
52
+ return readOwnerFile(options.fileSystem, ownerPath(options.lockDirectory));
53
+ }
54
+
55
+ function freshOwner(options, token) {
56
+ return {
57
+ leaseExpiresAt: options.now() + options.lockLeaseMilliseconds,
58
+ pid: options.pid,
59
+ sourceIdentity: options.sourceIdentity,
60
+ token,
61
+ };
62
+ }
63
+
64
+ async function writeNewOwner(fileSystem, directory, owner) {
65
+ try {
66
+ await fileSystem.writeFile(ownerPath(directory), `${JSON.stringify(owner)}\n`, {
67
+ flag: "wx",
68
+ });
69
+ } catch (error) {
70
+ fail("lock ownership metadata could not be written", error);
71
+ }
72
+ }
73
+
74
+ async function replaceOwner(options, owner) {
75
+ const target = ownerPath(options.lockDirectory);
76
+ const temporary = `${target}.tmp-${owner.token}`;
77
+ try {
78
+ await options.fileSystem.writeFile(temporary, `${JSON.stringify(owner)}\n`, {
79
+ flag: "wx",
80
+ });
81
+ await options.fileSystem.rename(temporary, target);
82
+ } catch (error) {
83
+ await options.fileSystem.rm(temporary, { force: true }).catch(() => {});
84
+ fail("lock lease renewal could not be persisted atomically", error);
85
+ }
86
+ }
87
+
88
+ function leaseIsActive(options, owner) {
89
+ return options.now() < owner.leaseExpiresAt;
90
+ }
91
+
92
+ function residuePrefixes(lockDirectory) {
93
+ const base = path.basename(lockDirectory);
94
+ return ["incomplete", "prepare", "release", "stale"].map(
95
+ (kind) => `${base}.${kind}-`,
96
+ );
97
+ }
98
+
99
+ async function cleanupResidues(options) {
100
+ const parent = path.dirname(options.lockDirectory);
101
+ const prefixes = residuePrefixes(options.lockDirectory);
102
+ const entries = await options.fileSystem.readdir(parent, { withFileTypes: true });
103
+ for (const entry of entries) {
104
+ const name = typeof entry === "string" ? entry : entry.name;
105
+ if (prefixes.some((prefix) => name.startsWith(prefix))) {
106
+ await options.fileSystem.rm(path.join(parent, name), {
107
+ force: true, recursive: true,
108
+ });
109
+ }
110
+ }
111
+ }
112
+
113
+ async function createFreshLock(options, owner) {
114
+ const prepared = `${options.lockDirectory}.prepare-${owner.token}`;
115
+ try {
116
+ await options.fileSystem.mkdir(prepared);
117
+ await writeNewOwner(options.fileSystem, prepared, owner);
118
+ await options.fileSystem.rename(prepared, options.lockDirectory);
119
+ } catch (error) {
120
+ await options.fileSystem.rm(prepared, { force: true, recursive: true })
121
+ .catch(() => {});
122
+ throw error;
123
+ }
124
+ await cleanupResidues(options);
125
+ }
126
+
127
+ async function removeOwnedClaim(options, token) {
128
+ const target = claimPath(options.lockDirectory);
129
+ let claim;
130
+ try {
131
+ claim = await readOwnerFile(options.fileSystem, target);
132
+ } catch (error) {
133
+ if (error?.cause?.code === "ENOENT") return;
134
+ throw error;
135
+ }
136
+ if (claim.token === token && claim.sourceIdentity === options.sourceIdentity) {
137
+ await options.fileSystem.rm(target, { force: true });
138
+ }
139
+ }
140
+
141
+ async function claimTransition(options, claimant) {
142
+ const target = claimPath(options.lockDirectory);
143
+ const temporary = path.join(options.lockDirectory,
144
+ `.reclaim-${claimant.token}.json`);
145
+ await options.fileSystem.writeFile(temporary, `${JSON.stringify(claimant)}\n`, {
146
+ flag: "wx",
147
+ });
148
+ try {
149
+ await options.fileSystem.link(temporary, target);
150
+ } catch (error) {
151
+ await options.fileSystem.rm(temporary, { force: true }).catch(() => {});
152
+ if (error?.code !== "EEXIST") fail("source lock transition claim failed", error);
153
+ const current = await readOwnerFile(options.fileSystem, target);
154
+ if (leaseIsActive(options, current)) {
155
+ fail("another command owns an active source lock transition", error);
156
+ }
157
+ await options.fileSystem.rm(target, { force: true });
158
+ return claimTransition(options, claimant);
159
+ }
160
+ await options.fileSystem.rm(temporary, { force: true });
161
+ return target;
162
+ }
163
+
164
+ async function heartbeat(options, token) {
165
+ const renewed = freshOwner(options, token);
166
+ await claimTransition(options, renewed);
167
+ try {
168
+ const current = await readOwner(options);
169
+ const claim = await readOwnerFile(
170
+ options.fileSystem, claimPath(options.lockDirectory),
171
+ );
172
+ if (current.token !== token || current.sourceIdentity !== options.sourceIdentity ||
173
+ claim.token !== token || claim.sourceIdentity !== options.sourceIdentity) {
174
+ fail("source lock ownership token was lost before the next mutation");
175
+ }
176
+ await replaceOwner(options, renewed);
177
+ } finally {
178
+ await removeOwnedClaim(options, token).catch(() => {});
179
+ }
180
+ }
181
+
182
+ async function reclaim(options, observed, claimant) {
183
+ await claimTransition(options, claimant);
184
+ const current = await readOwner(options);
185
+ const claim = await readOwnerFile(
186
+ options.fileSystem, claimPath(options.lockDirectory),
187
+ );
188
+ if (claim.token !== claimant.token || !leaseIsActive(options, claim)) {
189
+ fail("stale source lock reclaim ownership expired before quarantine");
190
+ }
191
+ if (current.sourceIdentity !== options.sourceIdentity ||
192
+ current.token !== observed.token || leaseIsActive(options, current)) {
193
+ await removeOwnedClaim(options, claimant.token).catch(() => {});
194
+ fail("source lock owner renewed or changed during stale reclaim");
195
+ }
196
+ const quarantine = `${options.lockDirectory}.stale-${claimant.token}`;
197
+ try {
198
+ await options.fileSystem.rename(options.lockDirectory, quarantine);
199
+ await createFreshLock(options, claimant);
200
+ } catch (error) {
201
+ await options.fileSystem.rm(quarantine, { force: true, recursive: true })
202
+ .catch(() => {});
203
+ if (existsError(error)) {
204
+ fail("another command acquired the fresh source lock during reclaim", error);
205
+ }
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ async function acquire(options) {
211
+ const token = options.tokenFactory();
212
+ if (typeof token !== "string" || !token) fail("lock token factory returned no token");
213
+ const claimant = freshOwner(options, token);
214
+ try {
215
+ await createFreshLock(options, claimant);
216
+ return token;
217
+ } catch (error) {
218
+ if (!existsError(error)) throw error;
219
+ }
220
+ const current = await readOwner(options);
221
+ if (current.sourceIdentity !== options.sourceIdentity) {
222
+ fail("existing lock belongs to a different source identity");
223
+ }
224
+ if (leaseIsActive(options, current)) {
225
+ fail(`an unexpired publication lease already exists for this source (PID ${current.pid})`);
226
+ }
227
+ await reclaim(options, current, claimant);
228
+ return token;
229
+ }
230
+
231
+ async function release(options, token) {
232
+ try {
233
+ await heartbeat(options, token);
234
+ } catch {
235
+ return "Source lock ownership could not be verified; the lock was retained.";
236
+ }
237
+ const quarantine = `${options.lockDirectory}.release-${token}`;
238
+ try {
239
+ await options.fileSystem.rename(options.lockDirectory, quarantine);
240
+ await options.fileSystem.rm(quarantine, { force: true, recursive: true });
241
+ } catch {
242
+ return `Source lock cleanup was retained for stale-owner recovery: ${quarantine}`;
243
+ }
244
+ }
245
+
246
+ export async function acquireToolcraftMotionReferenceLease(options) {
247
+ validateOptions(options);
248
+ const token = await acquire(options);
249
+ return {
250
+ heartbeat: () => heartbeat(options, token),
251
+ release: () => release(options, token),
252
+ token,
253
+ };
254
+ }
@@ -0,0 +1,130 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ import {
8
+ acquireToolcraftMotionReferenceLease,
9
+ } from "./toolcraft-motion-reference-publication-lease.mjs";
10
+
11
+ async function fixture(context) {
12
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "motion-lease-"));
13
+ context.after(() => fs.rm(root, { force: true, recursive: true }));
14
+ return { lockDirectory: path.join(root, "reference.lock") };
15
+ }
16
+
17
+ function settings(value, clock, token, fileSystem = fs) {
18
+ return {
19
+ fileSystem,
20
+ lockDirectory: value.lockDirectory,
21
+ lockLeaseMilliseconds: 100,
22
+ maximumUninterruptibleOperationMilliseconds: 40,
23
+ now: () => clock.value,
24
+ pid: token === "owner-a" ? 101 : 202,
25
+ sourceIdentity: "motion-reference-v1-lease-race",
26
+ tokenFactory: () => token,
27
+ };
28
+ }
29
+
30
+ test("a reclaim claimant rereads the owner lease after a racing heartbeat", async (context) => {
31
+ const value = await fixture(context);
32
+ const clock = { value: 1_000 };
33
+ const owner = await acquireToolcraftMotionReferenceLease(
34
+ settings(value, clock, "owner-a"),
35
+ );
36
+ clock.value = 1_150;
37
+ let reclaimLinkEntered;
38
+ let releaseReclaimLink;
39
+ const reclaimLinkStarted = new Promise((resolve) => {
40
+ reclaimLinkEntered = resolve;
41
+ });
42
+ const reclaimLinkGate = new Promise((resolve) => {
43
+ releaseReclaimLink = resolve;
44
+ });
45
+ const contenderFileSystem = {
46
+ ...fs,
47
+ link: async (...args) => {
48
+ reclaimLinkEntered();
49
+ await reclaimLinkGate;
50
+ return fs.link(...args);
51
+ },
52
+ };
53
+ const contender = acquireToolcraftMotionReferenceLease(
54
+ settings(value, clock, "owner-b", contenderFileSystem),
55
+ );
56
+ await reclaimLinkStarted;
57
+ await owner.heartbeat();
58
+ releaseReclaimLink();
59
+
60
+ await assert.rejects(contender, /renewed|active|lease/iu);
61
+ const persisted = JSON.parse(await fs.readFile(
62
+ path.join(value.lockDirectory, "owner.json"), "utf8",
63
+ ));
64
+ assert.equal(persisted.token, "owner-a");
65
+ assert.equal(persisted.leaseExpiresAt, 1_250);
66
+ assert.equal(await owner.release(), undefined);
67
+ });
68
+
69
+ test("a lock owner fails closed before mutation after its token is replaced", async (context) => {
70
+ const value = await fixture(context);
71
+ const clock = { value: 1_000 };
72
+ const owner = await acquireToolcraftMotionReferenceLease(
73
+ settings(value, clock, "owner-a"),
74
+ );
75
+ await fs.writeFile(path.join(value.lockDirectory, "owner.json"), `${JSON.stringify({
76
+ leaseExpiresAt: 1_200,
77
+ pid: 202,
78
+ sourceIdentity: "motion-reference-v1-lease-race",
79
+ token: "owner-b",
80
+ })}\n`);
81
+
82
+ await assert.rejects(owner.heartbeat(), /ownership|token|lost/iu);
83
+ const persisted = JSON.parse(await fs.readFile(
84
+ path.join(value.lockDirectory, "owner.json"), "utf8",
85
+ ));
86
+ assert.equal(persisted.token, "owner-b");
87
+ });
88
+
89
+ test("the declared uninterruptible operation bound stays within half a lease", async (context) => {
90
+ const value = await fixture(context);
91
+ const clock = { value: 1_000 };
92
+ await assert.rejects(() => acquireToolcraftMotionReferenceLease({
93
+ ...settings(value, clock, "owner-a"),
94
+ maximumUninterruptibleOperationMilliseconds: 51,
95
+ }), /operation bound|lease/iu);
96
+ await assert.rejects(fs.stat(value.lockDirectory), { code: "ENOENT" });
97
+ });
98
+
99
+ test("heartbeat persists the renewed owner by atomic sibling replacement", async (context) => {
100
+ const value = await fixture(context);
101
+ const clock = { value: 1_000 };
102
+ const operations = [];
103
+ const fileSystem = {
104
+ ...fs,
105
+ rename: async (from, to) => {
106
+ if (to === path.join(value.lockDirectory, "owner.json")) {
107
+ operations.push(["rename", from, to]);
108
+ }
109
+ return fs.rename(from, to);
110
+ },
111
+ writeFile: async (target, contents, options) => {
112
+ if (target.includes("owner.json.tmp-")) {
113
+ operations.push(["write", target, options]);
114
+ }
115
+ return fs.writeFile(target, contents, options);
116
+ },
117
+ };
118
+ const owner = await acquireToolcraftMotionReferenceLease(
119
+ settings(value, clock, "owner-a", fileSystem),
120
+ );
121
+ clock.value = 1_050;
122
+ await owner.heartbeat();
123
+
124
+ assert.equal(operations.length, 2);
125
+ assert.equal(operations[0][0], "write");
126
+ assert.equal(operations[0][2].flag, "wx");
127
+ assert.deepEqual(operations[1], ["rename", operations[0][1],
128
+ path.join(value.lockDirectory, "owner.json")]);
129
+ await owner.release();
130
+ });
@@ -0,0 +1,128 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ requireToolcraftMotionReferenceSourceIdentity,
5
+ } from "./toolcraft-motion-reference-roots.mjs";
6
+
7
+ function fail(message, cause) {
8
+ throw new Error(`Toolcraft motion reference staging scope failed: ${message}`, {
9
+ cause,
10
+ });
11
+ }
12
+
13
+ function isStrictDescendant(root, target) {
14
+ const relative = path.relative(root, target);
15
+ return relative !== "" && relative !== ".." &&
16
+ !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
17
+ }
18
+
19
+ function requireCanonicalAbsolute(target, label) {
20
+ if (typeof target !== "string" || !path.isAbsolute(target) ||
21
+ path.resolve(target) !== target) {
22
+ fail(`${label} must be a canonical absolute path without traversal`);
23
+ }
24
+ return target;
25
+ }
26
+
27
+ function publicationRecords(records) {
28
+ if (!Array.isArray(records) || records.length === 0) {
29
+ fail("a nonempty publication record group is required");
30
+ }
31
+ const published = records.filter(({ stagingDirectory }) =>
32
+ stagingDirectory !== null);
33
+ if (published.length === 0) {
34
+ fail("a publication group must contain at least one staged study");
35
+ }
36
+ return published;
37
+ }
38
+
39
+ export function defineToolcraftMotionReferenceStagingScope(
40
+ records,
41
+ { sourceIdentity, stagingRoot, temporaryRoot },
42
+ ) {
43
+ requireToolcraftMotionReferenceSourceIdentity(sourceIdentity);
44
+ requireCanonicalAbsolute(temporaryRoot, "temporary root");
45
+ const published = publicationRecords(records);
46
+ const derivedRoots = new Set(published.map(({ stagingDirectory }) => {
47
+ requireCanonicalAbsolute(stagingDirectory, "staging directory");
48
+ return path.dirname(stagingDirectory);
49
+ }));
50
+ if (derivedRoots.size !== 1) {
51
+ fail("one complete publication must use one canonical staging root");
52
+ }
53
+ const derivedRoot = derivedRoots.values().next().value;
54
+ if (stagingRoot !== undefined && stagingRoot !== derivedRoot) {
55
+ fail("declared staging root must own every staged study directly");
56
+ }
57
+ requireCanonicalAbsolute(derivedRoot, "staging root");
58
+ if (!isStrictDescendant(temporaryRoot, derivedRoot) ||
59
+ path.basename(derivedRoot) !== `${sourceIdentity}.staging`) {
60
+ fail("staging root must be source-owned beneath the checked temporary root");
61
+ }
62
+ for (const { finalDirectory, stagingDirectory } of published) {
63
+ requireCanonicalAbsolute(finalDirectory, "final directory");
64
+ if (path.dirname(stagingDirectory) !== derivedRoot ||
65
+ path.basename(stagingDirectory) !== path.basename(finalDirectory)) {
66
+ fail("each staged study must be the direct expected child of its staging root");
67
+ }
68
+ }
69
+ return { sourceIdentity, stagingRoot: derivedRoot, temporaryRoot };
70
+ }
71
+
72
+ async function requireDirectDirectoryChain(
73
+ fileSystem,
74
+ root,
75
+ target,
76
+ { allowMissingTarget },
77
+ ) {
78
+ const relative = path.relative(root, target);
79
+ let current = root;
80
+ for (const component of ["", ...relative.split(path.sep)]) {
81
+ if (component !== "") current = path.join(current, component);
82
+ let status;
83
+ try {
84
+ status = await fileSystem.lstat(current);
85
+ } catch (error) {
86
+ if (allowMissingTarget && error?.code === "ENOENT") return;
87
+ fail(`staging path is missing or unreadable: ${current}`, error);
88
+ }
89
+ if (!status.isDirectory() || status.isSymbolicLink()) {
90
+ fail(`staging path must contain only direct directories, not links: ${current}`);
91
+ }
92
+ }
93
+ const [realRoot, realTarget] = await Promise.all([
94
+ fileSystem.realpath(root),
95
+ fileSystem.realpath(target),
96
+ ]);
97
+ const contained = target === root ? realTarget === realRoot :
98
+ isStrictDescendant(realRoot, realTarget);
99
+ if (!contained) {
100
+ fail("staging path real location escapes its checked directory boundary");
101
+ }
102
+ }
103
+
104
+ export async function requireToolcraftMotionReferenceStagingScope(
105
+ records,
106
+ {
107
+ allowMissingStagingDirectories = false,
108
+ fileSystem,
109
+ sourceIdentity,
110
+ stagingRoot,
111
+ temporaryRoot,
112
+ },
113
+ ) {
114
+ const scope = defineToolcraftMotionReferenceStagingScope(records, {
115
+ sourceIdentity, stagingRoot, temporaryRoot,
116
+ });
117
+ await requireDirectDirectoryChain(
118
+ fileSystem, temporaryRoot, scope.stagingRoot,
119
+ { allowMissingTarget: allowMissingStagingDirectories },
120
+ );
121
+ for (const { stagingDirectory } of publicationRecords(records)) {
122
+ await requireDirectDirectoryChain(
123
+ fileSystem, scope.stagingRoot, stagingDirectory,
124
+ { allowMissingTarget: allowMissingStagingDirectories },
125
+ );
126
+ }
127
+ return scope;
128
+ }