@dailephd/my-dev-kit-lab 0.4.5 → 0.4.7

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 (263) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +43 -3
  3. package/dist/scripts/cli.js +9 -0
  4. package/dist/src/audits/core/typescriptJavaScriptAnalyzer.js +30 -12
  5. package/dist/src/browser/index.js +2 -0
  6. package/dist/src/browser/playwrightRuntime.js +74 -0
  7. package/dist/src/browser/types.js +10 -0
  8. package/dist/src/cli/help.js +376 -0
  9. package/dist/src/cli/index.js +2 -0
  10. package/dist/src/cli/runLabCli.js +257 -0
  11. package/dist/src/commands/runAuditCommand.js +132 -0
  12. package/dist/src/commands/runControlledExperimentCommand.js +7 -3
  13. package/dist/{scripts/experiments/describeExperiment.js → src/commands/runExperimentDescribeCommand.js} +14 -10
  14. package/dist/{scripts/experiments/listExperiments.js → src/commands/runExperimentListCommand.js} +3 -6
  15. package/dist/{scripts/experiments/runExperiment.js → src/commands/runExperimentRunCommand.js} +80 -22
  16. package/dist/src/commands/runFinalDemoCommand.js +2 -3
  17. package/dist/src/commands/runSecurityValidationCommand.js +271 -0
  18. package/dist/src/commands/runTutorialRunCommand.js +170 -0
  19. package/dist/src/commands/runTutorialValidateCommand.js +138 -0
  20. package/dist/src/core/processTree.js +52 -0
  21. package/dist/src/core/resolveCommand.js +34 -0
  22. package/dist/src/core/runMeasuredCommand.js +39 -31
  23. package/dist/src/runtime/index.js +4 -0
  24. package/dist/src/runtime/labExecutionContext.js +22 -0
  25. package/dist/src/runtime/managedProcess.js +380 -0
  26. package/dist/src/runtime/packageResource.js +22 -0
  27. package/dist/src/runtime/packageRoot.js +37 -0
  28. package/dist/src/screenshot/captureReportScreenshot.js +30 -27
  29. package/dist/src/securityValidation/packageChecks/requiredPackageContents.js +14 -1
  30. package/dist/src/tutorial/index.js +16 -0
  31. package/dist/src/tutorial/loadTutorialContracts.js +60 -0
  32. package/dist/src/tutorial/markdownTutorialWriter.js +53 -0
  33. package/dist/src/tutorial/runTutorial.js +465 -0
  34. package/dist/src/tutorial/scenarioValidation.js +383 -0
  35. package/dist/src/tutorial/subtitleWriter.js +88 -0
  36. package/dist/src/tutorial/targetContractValidation.js +212 -0
  37. package/dist/src/tutorial/tutorialActions.js +110 -0
  38. package/dist/src/tutorial/tutorialArtifacts.js +176 -0
  39. package/dist/src/tutorial/tutorialAssertions.js +249 -0
  40. package/dist/src/tutorial/tutorialCursor.js +149 -0
  41. package/dist/src/tutorial/tutorialLocators.js +37 -0
  42. package/dist/src/tutorial/tutorialManifest.js +74 -0
  43. package/dist/src/tutorial/tutorialOverlay.js +180 -0
  44. package/dist/src/tutorial/tutorialPaths.js +86 -0
  45. package/dist/src/tutorial/tutorialSession.js +398 -0
  46. package/dist/src/tutorial/types.js +48 -0
  47. package/dist/src/tutorial/validationPrimitives.js +141 -0
  48. package/examples/tutorial-browser/index.html +47 -0
  49. package/examples/tutorial-browser/prepare.mjs +14 -0
  50. package/examples/tutorial-browser/scenario.json +17 -0
  51. package/examples/tutorial-browser/server.mjs +14 -0
  52. package/package.json +13 -9
  53. package/dist/scripts/audits/runAudit.js +0 -135
  54. package/dist/scripts/build-gallery.js +0 -3
  55. package/dist/scripts/capture-demo-report.js +0 -3
  56. package/dist/scripts/evaluate-token-savings.js +0 -2
  57. package/dist/scripts/generate-experiment-plots.js +0 -3
  58. package/dist/scripts/generate-prompt-variants.js +0 -2
  59. package/dist/scripts/render-experiment-report.js +0 -2
  60. package/dist/scripts/report-context-integrity-smoke.js +0 -70
  61. package/dist/scripts/run-agent-prompt.js +0 -2
  62. package/dist/scripts/run-controlled-experiment.js +0 -2
  63. package/dist/scripts/run-lab-demo.js +0 -5
  64. package/dist/scripts/run-visualization-demos.js +0 -3
  65. package/dist/scripts/security/resolveToolRoot.js +0 -16
  66. package/dist/scripts/security/runCodeql.js +0 -58
  67. package/dist/scripts/security/runDependencyChecks.js +0 -58
  68. package/dist/scripts/security/runFuzzSmoke.js +0 -29
  69. package/dist/scripts/security/runPackageChecks.js +0 -57
  70. package/dist/scripts/security/runSemgrep.js +0 -64
  71. package/dist/scripts/security/validate.js +0 -266
  72. package/dist/scripts/verify-benchmarks.js +0 -202
  73. package/examples/demo-report-input.json +0 -78
  74. package/examples/lab-demo-cases.json +0 -35
  75. package/examples/real-agent-campaign-cases.json +0 -118
  76. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +0 -4
  77. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +0 -4
  78. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +0 -5
  79. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +0 -4
  80. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +0 -10
  81. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +0 -4
  82. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +0 -4
  83. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +0 -4
  84. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +0 -4
  85. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +0 -4
  86. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +0 -8
  87. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +0 -8
  88. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +0 -6
  89. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +0 -11
  90. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +0 -26
  91. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +0 -6
  92. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +0 -4
  93. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +0 -7
  94. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +0 -9
  95. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +0 -13
  96. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +0 -7
  97. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +0 -10
  98. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +0 -7
  99. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +0 -8
  100. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +0 -4
  101. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +0 -8
  102. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +0 -7
  103. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +0 -8
  104. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +0 -9
  105. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +0 -11
  106. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +0 -11
  107. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +0 -16
  108. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +0 -3
  109. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
  110. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +0 -1
  111. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +0 -1
  112. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +0 -1
  113. package/tests/fixtures/android/compose-app/app/build.gradle.kts +0 -21
  114. package/tests/fixtures/android/compose-app/app/src/main/AndroidManifest.xml +0 -22
  115. package/tests/fixtures/android/compose-app/app/src/main/java/com/example/composeapp/MainActivity.kt +0 -11
  116. package/tests/fixtures/android/compose-app/build.gradle.kts +0 -4
  117. package/tests/fixtures/android/compose-app/gradle/wrapper/gradle-wrapper.properties +0 -4
  118. package/tests/fixtures/android/compose-app/settings.gradle.kts +0 -2
  119. package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/build.gradle.kts +0 -3
  120. package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/gradle/libs.versions.toml +0 -5
  121. package/tests/fixtures/android/gradle-metadata-fixtures/comment-safety.gradle.kts +0 -21
  122. package/tests/fixtures/android/gradle-metadata-fixtures/dynamic-values.gradle.kts +0 -17
  123. package/tests/fixtures/android/gradle-metadata-fixtures/groovy-literal.gradle +0 -23
  124. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-malformed.properties +0 -4
  125. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-missing-url.properties +0 -3
  126. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-with-checksum.properties +0 -5
  127. package/tests/fixtures/android/java-app/app/build.gradle +0 -16
  128. package/tests/fixtures/android/java-app/app/src/main/AndroidManifest.xml +0 -19
  129. package/tests/fixtures/android/java-app/app/src/main/java/com/example/javaapp/MainActivity.java +0 -4
  130. package/tests/fixtures/android/java-app/build.gradle +0 -3
  131. package/tests/fixtures/android/java-app/gradle/wrapper/gradle-wrapper.properties +0 -4
  132. package/tests/fixtures/android/java-app/settings.gradle +0 -2
  133. package/tests/fixtures/android/library/build.gradle.kts +0 -3
  134. package/tests/fixtures/android/library/gradle/wrapper/gradle-wrapper.properties +0 -4
  135. package/tests/fixtures/android/library/mylibrary/build.gradle.kts +0 -13
  136. package/tests/fixtures/android/library/mylibrary/src/main/AndroidManifest.xml +0 -2
  137. package/tests/fixtures/android/library/settings.gradle.kts +0 -2
  138. package/tests/fixtures/android/manifest-audit-fixtures/incomplete-intent-filter.xml +0 -14
  139. package/tests/fixtures/android/manifest-audit-fixtures/malformed-exported.xml +0 -10
  140. package/tests/fixtures/android/manifest-audit-fixtures/malformed.xml +0 -5
  141. package/tests/fixtures/android/manifest-audit-fixtures/missing-application.xml +0 -7
  142. package/tests/fixtures/android/manifest-audit-fixtures/missing-component-name.xml +0 -11
  143. package/tests/fixtures/android/manifest-audit-fixtures/permission-coverage.xml +0 -29
  144. package/tests/fixtures/android/manifest-audit-fixtures/risky-components.xml +0 -62
  145. package/tests/fixtures/android/manifest-audit-fixtures/risky-deep-links.xml +0 -45
  146. package/tests/fixtures/android/manifest-audit-fixtures/unresolved-placeholder.xml +0 -14
  147. package/tests/fixtures/android/manifest-audit-fixtures/unusual-namespace-prefix.xml +0 -11
  148. package/tests/fixtures/android/multi-module/app/build.gradle.kts +0 -22
  149. package/tests/fixtures/android/multi-module/app/src/androidTest/java/com/example/multimodule/app/ExampleInstrumentedTest.kt +0 -3
  150. package/tests/fixtures/android/multi-module/app/src/main/AndroidManifest.xml +0 -11
  151. package/tests/fixtures/android/multi-module/app/src/test/java/com/example/multimodule/app/ExampleUnitTest.kt +0 -3
  152. package/tests/fixtures/android/multi-module/build.gradle.kts +0 -5
  153. package/tests/fixtures/android/multi-module/core/build.gradle.kts +0 -14
  154. package/tests/fixtures/android/multi-module/core/src/main/AndroidManifest.xml +0 -2
  155. package/tests/fixtures/android/multi-module/feature-login/build.gradle.kts +0 -18
  156. package/tests/fixtures/android/multi-module/feature-login/src/main/AndroidManifest.xml +0 -2
  157. package/tests/fixtures/android/multi-module/gradle/libs.versions.toml +0 -8
  158. package/tests/fixtures/android/multi-module/gradle/wrapper/gradle-wrapper.properties +0 -4
  159. package/tests/fixtures/android/multi-module/settings.gradle.kts +0 -4
  160. package/tests/fixtures/android/non-android-gradle/build.gradle.kts +0 -3
  161. package/tests/fixtures/android/non-android-gradle/core/build.gradle.kts +0 -3
  162. package/tests/fixtures/android/non-android-gradle/core/src/main/java/com/example/core/Core.java +0 -4
  163. package/tests/fixtures/android/non-android-gradle/gradle/wrapper/gradle-wrapper.properties +0 -4
  164. package/tests/fixtures/android/non-android-gradle/settings.gradle.kts +0 -2
  165. package/tests/fixtures/android/partial/app/build.gradle.kts +0 -7
  166. package/tests/fixtures/android/partial/settings.gradle.kts +0 -2
  167. package/tests/fixtures/android/xml-view-app/app/build.gradle.kts +0 -17
  168. package/tests/fixtures/android/xml-view-app/app/src/main/AndroidManifest.xml +0 -31
  169. package/tests/fixtures/android/xml-view-app/app/src/main/kotlin/com/example/xmlviewapp/MainActivity.kt +0 -7
  170. package/tests/fixtures/android/xml-view-app/app/src/main/res/layout/activity_main.xml +0 -4
  171. package/tests/fixtures/android/xml-view-app/build.gradle.kts +0 -4
  172. package/tests/fixtures/android/xml-view-app/gradle/wrapper/gradle-wrapper.properties +0 -4
  173. package/tests/fixtures/android/xml-view-app/settings.gradle.kts +0 -2
  174. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/README.md +0 -16
  175. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/build.gradle.kts +0 -4
  176. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/java/com/example/Widget.java +0 -7
  177. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/kotlin/com/example/Gadget.kt +0 -7
  178. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/README.md +0 -10
  179. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/build/Main.class +0 -1
  180. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/dist/index.js +0 -1
  181. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/generated/schema.ts +0 -1
  182. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/src/index.ts +0 -3
  183. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/vendor/lib.py +0 -2
  184. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/README.md +0 -10
  185. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/build.gradle.kts +0 -8
  186. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/settings.gradle.kts +0 -1
  187. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/java/com/example/FooService.java +0 -7
  188. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/kotlin/com/example/BarService.kt +0 -7
  189. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/java/com/example/FooServiceTest.java +0 -8
  190. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/kotlin/com/example/BarServiceTest.kt +0 -8
  191. package/tests/fixtures/audits/mixed-language/mixed-python-ts/README.md +0 -8
  192. package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/app.py +0 -7
  193. package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/index.ts +0 -3
  194. package/tests/fixtures/audits/mixed-language/mixed-python-ts/tests/test_app.py +0 -5
  195. package/tests/fixtures/audits/mixed-language/mixed-ts-js/README.md +0 -7
  196. package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/helper.js +0 -5
  197. package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/index.ts +0 -3
  198. package/tests/fixtures/audits/mixed-language/mixed-ts-js/tests/index.sample.ts +0 -5
  199. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/README.md +0 -10
  200. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/build.gradle.kts +0 -4
  201. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/pytests/test_app.py +0 -5
  202. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/app.py +0 -2
  203. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/helper.js +0 -5
  204. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/index.ts +0 -3
  205. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/java/com/example/Multiplier.java +0 -7
  206. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/kotlin/com/example/Divider.kt +0 -7
  207. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/corrected-replay/derived/run-integrity-evidence.json +0 -99
  208. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/corrected-replay/producer/implementation-audit.json +0 -1585
  209. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/corrected-replay/producer/implementation-capsule.json +0 -11455
  210. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/00-request.txt +0 -1
  211. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/architecture-context-packet.txt +0 -82
  212. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/behavior-model.txt +0 -104
  213. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/final-report.txt +0 -41
  214. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/implementation-context-packet.txt +0 -114
  215. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/implementation-report.txt +0 -92
  216. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/judge-report.txt +0 -34
  217. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/pseudocode-packet.txt +0 -244
  218. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/request-brief.txt +0 -40
  219. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/test-context-packet.txt +0 -93
  220. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/test-implementation-report.txt +0 -69
  221. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/test-strategy-packet.txt +0 -354
  222. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/artifacts/verification-report.txt +0 -85
  223. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/derived/run-integrity-evidence.json +0 -98
  224. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/producer/architecture-audit.json +0 -1169
  225. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/producer/architecture-request.json +0 -29
  226. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/producer/implementation-audit.json +0 -1494
  227. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/producer/implementation-capsule.json +0 -11364
  228. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/producer/implementation-request.json +0 -33
  229. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/producer/test-request.json +0 -74
  230. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/reports/architecture-context-retrieval-report.txt +0 -58
  231. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/reports/implementation-context-retrieval-report.txt +0 -75
  232. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/reports/test-context-retrieval-report.txt +0 -84
  233. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/run.json +0 -62
  234. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/tool-versions/my-dev-kit-version.txt +0 -1
  235. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/failed-run/tool-versions/orchestrator-version.txt +0 -1
  236. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/manifests/corrected-replay-manifest.json +0 -19
  237. package/tests/fixtures/ecosystem/context-integrity/v0.4.5/manifests/failed-run-manifest.json +0 -56
  238. package/tests/fixtures/fake-adversarial-cli.js +0 -152
  239. package/tests/fixtures/fake-my-dev-kit-cli.js +0 -83
  240. package/tests/fixtures/full-workflow-library/complete-v1.0.0.json +0 -24
  241. package/tests/fixtures/full-workflow-library/future-minor-v1.1.0-additive.json +0 -25
  242. package/tests/fixtures/stage-context-expectations/complete-v1.0.0.json +0 -230
  243. package/tests/fixtures/stage-context-expectations/future-minor-v1.1.0-additive.json +0 -232
  244. package/tests/fixtures/stage-context-expectations/invalid/malformed-json.txt +0 -1
  245. package/tests/fixtures/stage-context-expectations/invalid/non-object-root.json +0 -1
  246. package/tests/fixtures/stage-context-expectations/minimal-v1.0.0.json +0 -18
  247. package/tests/fixtures/upstream-artifacts/invalid/malformed-json.txt +0 -1
  248. package/tests/fixtures/upstream-artifacts/invalid/non-object-root.json +0 -1
  249. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/context-capsule/complete-v1.0.0.json +0 -504
  250. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/context-capsule/future-minor-v1.1.0-additive.json +0 -955
  251. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/retrieval-audit-record/complete-v1.0.0.json +0 -190
  252. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/retrieval-audit-record/future-minor-v1.1.0-additive.json +0 -327
  253. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.4/PROVENANCE.txt +0 -18
  254. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.4/context-capsule/complete-v1.0.0.json +0 -556
  255. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.4/retrieval-audit-record/complete-v1.0.0.json +0 -238
  256. package/tests/fixtures/upstream-artifacts/my-dev-kit-1.10.2-identity/PROVENANCE.txt +0 -13
  257. package/tests/fixtures/upstream-artifacts/my-dev-kit-1.10.2-identity/context-capsule.json +0 -7874
  258. package/tests/fixtures/upstream-artifacts/my-dev-kit-1.10.2-identity/retrieval-audit-record.json +0 -1076
  259. package/tests/fixtures/upstream-artifacts/my-dev-kit-orchestrator/1.2.1/workflow-instruction-packet/complete-v1.0.0.json +0 -160
  260. package/tests/fixtures/upstream-artifacts/my-dev-kit-orchestrator/1.2.1/workflow-instruction-packet/future-minor-v1.1.0-additive.json +0 -186
  261. package/tests/fixtures/upstream-artifacts/my-dev-kit-orchestrator/1.2.3/run-integrity/PROVENANCE.txt +0 -31
  262. package/tests/fixtures/upstream-artifacts/my-dev-kit-orchestrator/1.2.3/run-integrity/ready-pass-final-pass.json +0 -132
  263. package/tests/fixtures/upstream-artifacts/my-dev-kit-orchestrator/1.2.3/run-integrity/refresh-required-need-context.json +0 -146
@@ -0,0 +1,383 @@
1
+ import { MAX_STEP_PAUSE_MS, MAX_TUTORIAL_STEPS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, MIN_VIEWPORT_HEIGHT, MIN_VIEWPORT_WIDTH, TUTORIAL_ACTION_TYPES, TUTORIAL_ASSERTION_TYPES, TUTORIAL_CALLOUT_PLACEMENTS, TUTORIAL_GOTO_WAIT_UNTIL, TUTORIAL_LOCATOR_KINDS, TUTORIAL_SCHEMA_VERSION, TUTORIAL_WAIT_FOR_STATES } from "./types.js";
2
+ import { createErrorCollector, describeValue, isJsonScalar, isNonEmptyString, isNonNegativeInteger, isPlainObject, rejectUnknownKeys, validateOptionalTimeout, validateSchemaVersion, validateStableId } from "./validationPrimitives.js";
3
+ const SCENARIO_KEYS = ["schemaVersion", "id", "title", "description", "targetId", "browser", "steps"];
4
+ const STEP_KEYS = [
5
+ "id",
6
+ "narration",
7
+ "pauseBeforeMs",
8
+ "pauseAfterMs",
9
+ "action",
10
+ "highlight",
11
+ "callout",
12
+ "screenshot",
13
+ "assertions"
14
+ ];
15
+ /**
16
+ * Validates a parsed JSON value as a TutorialScenarioV1.
17
+ *
18
+ * Strict by construction: unknown action, assertion and locator kinds fail, and
19
+ * unknown object fields fail rather than being ignored. That closure is what
20
+ * keeps a scenario from smuggling in an execution escape hatch (an `evaluate`
21
+ * action, a `script` field, an absolute `goto` URL) that a permissive validator
22
+ * would quietly drop on the floor.
23
+ *
24
+ * `source` is used to prefix each message, normally the scenario file path.
25
+ */
26
+ export function validateTutorialScenario(value, source = "Scenario") {
27
+ const errors = createErrorCollector(source);
28
+ if (!isPlainObject(value)) {
29
+ errors.add("<root>", `expected a JSON object, received ${describeValue(value)}.`);
30
+ return { ok: false, errors: errors.errors };
31
+ }
32
+ rejectUnknownKeys(errors, "<root>", value, SCENARIO_KEYS);
33
+ validateSchemaVersion(errors, value.schemaVersion, TUTORIAL_SCHEMA_VERSION);
34
+ validateStableId(errors, "id", value.id);
35
+ validateStableId(errors, "targetId", value.targetId);
36
+ if (!isNonEmptyString(value.title)) {
37
+ errors.add("title", `expected a non-empty title string, received ${describeValue(value.title)}.`);
38
+ }
39
+ if (value.description !== undefined && typeof value.description !== "string") {
40
+ errors.add("description", `expected a string when supplied, received ${describeValue(value.description)}.`);
41
+ }
42
+ validateBrowser(errors, value.browser);
43
+ validateSteps(errors, value.steps);
44
+ if (errors.length > 0) {
45
+ return { ok: false, errors: errors.errors };
46
+ }
47
+ return { ok: true, value: value };
48
+ }
49
+ function validateBrowser(errors, value) {
50
+ if (!isPlainObject(value)) {
51
+ errors.add("browser", `expected an object with a viewport, received ${describeValue(value)}.`);
52
+ return;
53
+ }
54
+ rejectUnknownKeys(errors, "browser", value, ["viewport"]);
55
+ const viewport = value.viewport;
56
+ if (!isPlainObject(viewport)) {
57
+ errors.add("browser.viewport", `expected an object with width and height, received ${describeValue(viewport)}.`);
58
+ return;
59
+ }
60
+ rejectUnknownKeys(errors, "browser.viewport", viewport, ["width", "height"]);
61
+ validateBounded(errors, "browser.viewport.width", viewport.width, MIN_VIEWPORT_WIDTH, MAX_VIEWPORT_WIDTH);
62
+ validateBounded(errors, "browser.viewport.height", viewport.height, MIN_VIEWPORT_HEIGHT, MAX_VIEWPORT_HEIGHT);
63
+ }
64
+ function validateBounded(errors, location, value, min, max) {
65
+ if (typeof value !== "number" || !Number.isInteger(value)) {
66
+ errors.add(location, `expected a finite integer, received ${describeValue(value)}.`);
67
+ return;
68
+ }
69
+ if (value < min || value > max) {
70
+ errors.add(location, `expected an integer between ${min} and ${max}, received ${value}.`);
71
+ }
72
+ }
73
+ function validateSteps(errors, value) {
74
+ if (!Array.isArray(value)) {
75
+ errors.add("steps", `expected an array of steps, received ${describeValue(value)}.`);
76
+ return;
77
+ }
78
+ if (value.length === 0) {
79
+ errors.add("steps", "a scenario must declare at least one step.");
80
+ return;
81
+ }
82
+ if (value.length > MAX_TUTORIAL_STEPS) {
83
+ errors.add("steps", `a scenario may declare at most ${MAX_TUTORIAL_STEPS} steps, received ${value.length}.`);
84
+ return;
85
+ }
86
+ const stepIds = new Set();
87
+ const screenshotIds = new Set();
88
+ value.forEach((step, index) => {
89
+ validateStep(errors, `steps[${index}]`, step, stepIds, screenshotIds);
90
+ });
91
+ }
92
+ function validateStep(errors, location, value, stepIds, screenshotIds) {
93
+ if (!isPlainObject(value)) {
94
+ errors.add(location, `expected a step object, received ${describeValue(value)}.`);
95
+ return;
96
+ }
97
+ rejectUnknownKeys(errors, location, value, STEP_KEYS);
98
+ if (validateStableId(errors, `${location}.id`, value.id)) {
99
+ if (stepIds.has(value.id)) {
100
+ errors.add(`${location}.id`, `duplicate step id ${JSON.stringify(value.id)}.`);
101
+ }
102
+ stepIds.add(value.id);
103
+ }
104
+ if (!isNonEmptyString(value.narration)) {
105
+ errors.add(`${location}.narration`, `expected non-empty narration text after trimming, received ${describeValue(value.narration)}.`);
106
+ }
107
+ validatePause(errors, `${location}.pauseBeforeMs`, value.pauseBeforeMs);
108
+ validatePause(errors, `${location}.pauseAfterMs`, value.pauseAfterMs);
109
+ if (value.action !== undefined) {
110
+ validateAction(errors, `${location}.action`, value.action);
111
+ }
112
+ if (value.highlight !== undefined) {
113
+ validateLocator(errors, `${location}.highlight`, value.highlight);
114
+ }
115
+ if (value.callout !== undefined) {
116
+ validateCallout(errors, `${location}.callout`, value.callout);
117
+ }
118
+ if (value.screenshot !== undefined) {
119
+ validateScreenshot(errors, `${location}.screenshot`, value.screenshot, screenshotIds);
120
+ }
121
+ if (value.assertions !== undefined) {
122
+ if (!Array.isArray(value.assertions)) {
123
+ errors.add(`${location}.assertions`, `expected an array of assertions, received ${describeValue(value.assertions)}.`);
124
+ }
125
+ else {
126
+ value.assertions.forEach((assertion, index) => {
127
+ validateAssertion(errors, `${location}.assertions[${index}]`, assertion);
128
+ });
129
+ }
130
+ }
131
+ }
132
+ function validatePause(errors, location, value) {
133
+ if (value === undefined) {
134
+ return;
135
+ }
136
+ if (!isNonNegativeInteger(value)) {
137
+ errors.add(location, `expected a finite non-negative integer number of milliseconds, received ${describeValue(value)}.`);
138
+ return;
139
+ }
140
+ if (value > MAX_STEP_PAUSE_MS) {
141
+ errors.add(location, `expected at most ${MAX_STEP_PAUSE_MS}ms, received ${value}.`);
142
+ }
143
+ }
144
+ function validateCallout(errors, location, value) {
145
+ if (!isPlainObject(value)) {
146
+ errors.add(location, `expected a callout object, received ${describeValue(value)}.`);
147
+ return;
148
+ }
149
+ rejectUnknownKeys(errors, location, value, ["text", "locator", "placement"]);
150
+ if (!isNonEmptyString(value.text)) {
151
+ errors.add(`${location}.text`, `expected non-empty callout text, received ${describeValue(value.text)}.`);
152
+ }
153
+ if (value.locator !== undefined) {
154
+ validateLocator(errors, `${location}.locator`, value.locator);
155
+ }
156
+ if (value.placement !== undefined && !TUTORIAL_CALLOUT_PLACEMENTS.includes(value.placement)) {
157
+ errors.add(`${location}.placement`, `expected one of ${TUTORIAL_CALLOUT_PLACEMENTS.join(", ")}, received ${describeValue(value.placement)}.`);
158
+ }
159
+ }
160
+ function validateScreenshot(errors, location, value, screenshotIds) {
161
+ if (!isPlainObject(value)) {
162
+ errors.add(location, `expected a screenshot object, received ${describeValue(value)}.`);
163
+ return;
164
+ }
165
+ rejectUnknownKeys(errors, location, value, ["id", "fullPage"]);
166
+ if (validateStableId(errors, `${location}.id`, value.id)) {
167
+ if (screenshotIds.has(value.id)) {
168
+ errors.add(`${location}.id`, `duplicate screenshot id ${JSON.stringify(value.id)}.`);
169
+ }
170
+ screenshotIds.add(value.id);
171
+ }
172
+ if (value.fullPage !== undefined && typeof value.fullPage !== "boolean") {
173
+ errors.add(`${location}.fullPage`, `expected a boolean, received ${describeValue(value.fullPage)}.`);
174
+ }
175
+ }
176
+ export function validateLocator(errors, location, value) {
177
+ if (!isPlainObject(value)) {
178
+ errors.add(location, `expected a locator object, received ${describeValue(value)}.`);
179
+ return;
180
+ }
181
+ const kind = value.kind;
182
+ if (typeof kind !== "string" || !TUTORIAL_LOCATOR_KINDS.includes(kind)) {
183
+ errors.add(`${location}.kind`, `unsupported locator kind ${describeValue(kind)}; expected one of ${TUTORIAL_LOCATOR_KINDS.join(", ")}.`);
184
+ return;
185
+ }
186
+ switch (kind) {
187
+ case "role":
188
+ rejectUnknownKeys(errors, location, value, ["kind", "role", "name", "exact"]);
189
+ if (!isNonEmptyString(value.role)) {
190
+ errors.add(`${location}.role`, `expected a non-empty role, received ${describeValue(value.role)}.`);
191
+ }
192
+ if (value.name !== undefined && !isNonEmptyString(value.name)) {
193
+ errors.add(`${location}.name`, `expected a non-empty accessible name when supplied, received ${describeValue(value.name)}.`);
194
+ }
195
+ validateOptionalBoolean(errors, `${location}.exact`, value.exact);
196
+ break;
197
+ case "text":
198
+ rejectUnknownKeys(errors, location, value, ["kind", "text", "exact"]);
199
+ if (!isNonEmptyString(value.text)) {
200
+ errors.add(`${location}.text`, `expected non-empty text, received ${describeValue(value.text)}.`);
201
+ }
202
+ validateOptionalBoolean(errors, `${location}.exact`, value.exact);
203
+ break;
204
+ case "css":
205
+ rejectUnknownKeys(errors, location, value, ["kind", "selector"]);
206
+ if (!isNonEmptyString(value.selector)) {
207
+ errors.add(`${location}.selector`, `expected a non-empty CSS selector, received ${describeValue(value.selector)}.`);
208
+ }
209
+ break;
210
+ case "test-id":
211
+ rejectUnknownKeys(errors, location, value, ["kind", "testId"]);
212
+ if (!isNonEmptyString(value.testId)) {
213
+ errors.add(`${location}.testId`, `expected a non-empty testId, received ${describeValue(value.testId)}.`);
214
+ }
215
+ break;
216
+ }
217
+ }
218
+ function validateOptionalBoolean(errors, location, value) {
219
+ if (value !== undefined && typeof value !== "boolean") {
220
+ errors.add(location, `expected a boolean, received ${describeValue(value)}.`);
221
+ }
222
+ }
223
+ export function validateAction(errors, location, value) {
224
+ if (!isPlainObject(value)) {
225
+ errors.add(location, `expected an action object, received ${describeValue(value)}.`);
226
+ return;
227
+ }
228
+ const type = value.type;
229
+ if (typeof type !== "string" || !TUTORIAL_ACTION_TYPES.includes(type)) {
230
+ errors.add(`${location}.type`, `unsupported action ${describeValue(type)}; expected one of ${TUTORIAL_ACTION_TYPES.join(", ")}.`);
231
+ return;
232
+ }
233
+ switch (type) {
234
+ case "goto":
235
+ rejectUnknownKeys(errors, location, value, ["type", "path", "waitUntil"]);
236
+ validateGotoPath(errors, `${location}.path`, value.path);
237
+ if (value.waitUntil !== undefined && !TUTORIAL_GOTO_WAIT_UNTIL.includes(value.waitUntil)) {
238
+ errors.add(`${location}.waitUntil`, `expected one of ${TUTORIAL_GOTO_WAIT_UNTIL.join(", ")}, received ${describeValue(value.waitUntil)}.`);
239
+ }
240
+ break;
241
+ case "click":
242
+ case "hover":
243
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "timeoutMs"]);
244
+ validateLocator(errors, `${location}.locator`, value.locator);
245
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
246
+ break;
247
+ case "fill":
248
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "value", "timeoutMs"]);
249
+ validateLocator(errors, `${location}.locator`, value.locator);
250
+ if (typeof value.value !== "string") {
251
+ errors.add(`${location}.value`, `expected a string value to fill, received ${describeValue(value.value)}.`);
252
+ }
253
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
254
+ break;
255
+ case "press":
256
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "key", "timeoutMs"]);
257
+ validateLocator(errors, `${location}.locator`, value.locator);
258
+ if (!isNonEmptyString(value.key)) {
259
+ errors.add(`${location}.key`, `expected a non-empty key name, received ${describeValue(value.key)}.`);
260
+ }
261
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
262
+ break;
263
+ case "drag":
264
+ rejectUnknownKeys(errors, location, value, ["type", "source", "target", "timeoutMs"]);
265
+ validateLocator(errors, `${location}.source`, value.source);
266
+ validateLocator(errors, `${location}.target`, value.target);
267
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
268
+ break;
269
+ case "wait-for":
270
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "state", "timeoutMs"]);
271
+ validateLocator(errors, `${location}.locator`, value.locator);
272
+ if (value.state !== undefined && !TUTORIAL_WAIT_FOR_STATES.includes(value.state)) {
273
+ errors.add(`${location}.state`, `expected one of ${TUTORIAL_WAIT_FOR_STATES.join(", ")}, received ${describeValue(value.state)}.`);
274
+ }
275
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
276
+ break;
277
+ }
278
+ }
279
+ /**
280
+ * A scenario may only navigate inside the target application's own origin, so a
281
+ * goto path is a root-relative path, never a URL. Rejecting the URL forms here
282
+ * (rather than only at resolution time) means an unsafe scenario fails
283
+ * `tutorial validate` without any process ever being started.
284
+ */
285
+ export function validateGotoPath(errors, location, value) {
286
+ if (typeof value !== "string" || value.length === 0) {
287
+ errors.add(location, `expected a root-relative path beginning with "/", received ${describeValue(value)}.`);
288
+ return;
289
+ }
290
+ if (value.startsWith("//")) {
291
+ errors.add(location, `protocol-relative paths are not allowed; received ${JSON.stringify(value)}. Use a root-relative path such as "/dashboard".`);
292
+ return;
293
+ }
294
+ if (!value.startsWith("/")) {
295
+ errors.add(location, `expected a root-relative path beginning with "/", received ${JSON.stringify(value)}. Absolute URLs and scheme-qualified values are not allowed.`);
296
+ }
297
+ }
298
+ export function validateAssertion(errors, location, value) {
299
+ if (!isPlainObject(value)) {
300
+ errors.add(location, `expected an assertion object, received ${describeValue(value)}.`);
301
+ return;
302
+ }
303
+ const type = value.type;
304
+ if (typeof type !== "string" || !TUTORIAL_ASSERTION_TYPES.includes(type)) {
305
+ errors.add(`${location}.type`, `unsupported assertion ${describeValue(type)}; expected one of ${TUTORIAL_ASSERTION_TYPES.join(", ")}.`);
306
+ return;
307
+ }
308
+ switch (type) {
309
+ case "element-visible":
310
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "timeoutMs"]);
311
+ validateLocator(errors, `${location}.locator`, value.locator);
312
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
313
+ break;
314
+ case "text-equals":
315
+ case "text-contains":
316
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "expected", "timeoutMs"]);
317
+ validateLocator(errors, `${location}.locator`, value.locator);
318
+ if (typeof value.expected !== "string") {
319
+ errors.add(`${location}.expected`, `expected a string, received ${describeValue(value.expected)}.`);
320
+ }
321
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
322
+ break;
323
+ case "url-path-equals":
324
+ rejectUnknownKeys(errors, location, value, ["type", "expected"]);
325
+ if (typeof value.expected !== "string" || !value.expected.startsWith("/")) {
326
+ errors.add(`${location}.expected`, `expected a root-relative path beginning with "/", received ${describeValue(value.expected)}.`);
327
+ }
328
+ break;
329
+ case "attribute-equals":
330
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "name", "expected", "timeoutMs"]);
331
+ validateLocator(errors, `${location}.locator`, value.locator);
332
+ if (!isNonEmptyString(value.name)) {
333
+ errors.add(`${location}.name`, `expected a non-empty attribute name, received ${describeValue(value.name)}.`);
334
+ }
335
+ if (typeof value.expected !== "string") {
336
+ errors.add(`${location}.expected`, `expected a string, received ${describeValue(value.expected)}.`);
337
+ }
338
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
339
+ break;
340
+ case "http-json-equals":
341
+ rejectUnknownKeys(errors, location, value, ["type", "path", "pointer", "expected", "timeoutMs"]);
342
+ validateGotoPath(errors, `${location}.path`, value.path);
343
+ validateJsonPointerDeclaration(errors, `${location}.pointer`, value.pointer);
344
+ if (!isJsonScalar(value.expected)) {
345
+ errors.add(`${location}.expected`, `expected a JSON scalar (string, finite number, boolean or null), received ${describeValue(value.expected)}.`);
346
+ }
347
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
348
+ break;
349
+ case "json-file-equals":
350
+ rejectUnknownKeys(errors, location, value, ["type", "path", "pointer", "expected"]);
351
+ validateTargetRelativePath(errors, `${location}.path`, value.path);
352
+ validateJsonPointerDeclaration(errors, `${location}.pointer`, value.pointer);
353
+ if (!isJsonScalar(value.expected)) {
354
+ errors.add(`${location}.expected`, `expected a JSON scalar (string, finite number, boolean or null), received ${describeValue(value.expected)}.`);
355
+ }
356
+ break;
357
+ case "file-exists":
358
+ rejectUnknownKeys(errors, location, value, ["type", "path"]);
359
+ validateTargetRelativePath(errors, `${location}.path`, value.path);
360
+ break;
361
+ }
362
+ }
363
+ function validateTargetRelativePath(errors, location, value) {
364
+ if (typeof value !== "string" || value.trim().length === 0) {
365
+ errors.add(location, `expected a non-empty path relative to the tutorial target root, received ${describeValue(value)}.`);
366
+ }
367
+ }
368
+ function validateJsonPointerDeclaration(errors, location, value) {
369
+ if (typeof value !== "string") {
370
+ errors.add(location, `expected a JSON Pointer string, received ${describeValue(value)}.`);
371
+ return;
372
+ }
373
+ if (value.length > 0 && !value.startsWith("/")) {
374
+ errors.add(location, `expected a JSON Pointer that is empty or begins with "/", received ${JSON.stringify(value)}.`);
375
+ return;
376
+ }
377
+ // Reject a malformed escape early so a bad pointer fails validation rather
378
+ // than silently never matching at assertion time.
379
+ const badEscape = /~(?![01])/.exec(value);
380
+ if (badEscape) {
381
+ errors.add(location, `malformed JSON Pointer escape at index ${badEscape.index}; "~" must be followed by "0" or "1" in ${JSON.stringify(value)}.`);
382
+ }
383
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Subtitle generation.
3
+ *
4
+ * Cues are built from the structured step results plus the scenario's own
5
+ * narration. This module never reads tutorial.md or tutorial-manifest.json to
6
+ * reconstruct text or timing -- there is exactly one narration source
7
+ * (`TutorialStepV1.narration`) and exactly one timing source (the recorded
8
+ * step timeline), and both arrive as data.
9
+ */
10
+ /** A cue shorter than this is unreadable, so short steps are padded for display. */
11
+ export const MIN_SUBTITLE_DURATION_MS = 500;
12
+ /**
13
+ * One cue per executed step that has narration and a recorded timeline.
14
+ *
15
+ * Not-run steps produce no cue: they never appeared in the recording. A cue is
16
+ * extended to MIN_SUBTITLE_DURATION_MS when the step was faster than that, but
17
+ * never past the next cue's start, so cues stay ordered and non-overlapping.
18
+ * The padding affects subtitle output only; step timing evidence is untouched.
19
+ */
20
+ export function buildSubtitleCues(options) {
21
+ const narrationByStepId = new Map(options.scenario.steps.map((step) => [step.id, step.narration]));
22
+ const raw = [];
23
+ for (const step of options.steps) {
24
+ if (step.status === "not-run") {
25
+ continue;
26
+ }
27
+ if (step.timelineStartMs === undefined || step.timelineEndMs === undefined) {
28
+ continue;
29
+ }
30
+ const text = (narrationByStepId.get(step.id) ?? "").trim();
31
+ if (text.length === 0) {
32
+ continue;
33
+ }
34
+ raw.push({
35
+ stepId: step.id,
36
+ startMs: Math.max(0, step.timelineStartMs),
37
+ endMs: Math.max(step.timelineStartMs, step.timelineEndMs),
38
+ text
39
+ });
40
+ }
41
+ return raw.map((cue, position) => {
42
+ let endMs = cue.endMs;
43
+ if (endMs - cue.startMs < MIN_SUBTITLE_DURATION_MS) {
44
+ endMs = cue.startMs + MIN_SUBTITLE_DURATION_MS;
45
+ }
46
+ const next = raw[position + 1];
47
+ if (next && endMs > next.startMs) {
48
+ // Cap at the next cue rather than overlap; never invert the cue.
49
+ endMs = Math.max(cue.startMs, next.startMs);
50
+ }
51
+ return { index: position + 1, stepId: cue.stepId, startMs: cue.startMs, endMs, text: cue.text };
52
+ });
53
+ }
54
+ /** `HH:MM:SS,mmm` (SRT) or `HH:MM:SS.mmm` (WebVTT). */
55
+ export function formatSubtitleTimestamp(totalMs, millisecondSeparator) {
56
+ const safeMs = Math.max(0, Math.round(totalMs));
57
+ const hours = Math.floor(safeMs / 3_600_000);
58
+ const minutes = Math.floor((safeMs % 3_600_000) / 60_000);
59
+ const seconds = Math.floor((safeMs % 60_000) / 1000);
60
+ const milliseconds = safeMs % 1000;
61
+ return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}${millisecondSeparator}${pad(milliseconds, 3)}`;
62
+ }
63
+ function pad(value, width) {
64
+ return String(value).padStart(width, "0");
65
+ }
66
+ /** Deterministic LF line endings regardless of host platform. */
67
+ export function renderSrt(cues) {
68
+ if (cues.length === 0) {
69
+ return "";
70
+ }
71
+ const blocks = cues.map((cue) => [
72
+ String(cue.index),
73
+ `${formatSubtitleTimestamp(cue.startMs, ",")} --> ${formatSubtitleTimestamp(cue.endMs, ",")}`,
74
+ cue.text
75
+ ].join("\n"));
76
+ return `${blocks.join("\n\n")}\n`;
77
+ }
78
+ export function renderVtt(cues) {
79
+ const header = "WEBVTT\n";
80
+ if (cues.length === 0) {
81
+ return header;
82
+ }
83
+ const blocks = cues.map((cue) => [
84
+ `${formatSubtitleTimestamp(cue.startMs, ".")} --> ${formatSubtitleTimestamp(cue.endMs, ".")}`,
85
+ cue.text
86
+ ].join("\n"));
87
+ return `${header}\n${blocks.join("\n\n")}\n`;
88
+ }