@pixel-point/toolcraft 0.0.16 → 0.0.18
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.
- package/package.json +1 -1
- package/src/generate-performance-iteration-execution.test.mjs +2 -2
- package/src/generate-test-demand-only-lifecycle.mjs +14 -3
- package/src/generate-test-dependency-sandbox.mjs +42 -5
- package/src/generate-test-doc-assertions.mjs +109 -69
- package/src/generate-test-doc-assertions.test.mjs +65 -0
- package/src/generate-test-doc-policy-assertions.mjs +92 -0
- package/src/generate-test-file-assertions.mjs +36 -118
- package/src/generate-test-output-assertions.mjs +95 -0
- package/src/generate-test-package-assertions.mjs +6 -0
- package/src/generate-test-performance-boundary-assertions.mjs +45 -0
- package/src/generate-test-product-fixture.mjs +1 -0
- package/src/generate-test-product-fixture.test.mjs +18 -0
- package/src/generate-test-product-iteration.mjs +1 -0
- package/src/generate-test-product-iteration.test.mjs +18 -0
- package/src/generate-test-runtime-boundary-assertions.mjs +158 -0
- package/src/generate-test-standalone-assertions.mjs +10 -0
- package/src/generate-test-standalone-delivery-stages.mjs +14 -6
- package/src/generate.test.mjs +14 -0
- package/templates/runtime/contracts/component-contracts.choices.test.ts +59 -12
- package/templates/runtime/contracts/component-contracts.choices.ts +58 -12
- package/templates/runtime/contracts/component-contracts.inputs.test.ts +6 -6
- package/templates/runtime/contracts/component-contracts.inputs.ts +6 -6
- package/templates/runtime/contracts/component-contracts.media-custom.test.ts +8 -2
- package/templates/runtime/contracts/component-contracts.media-custom.ts +8 -2
- package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +21 -6
- package/templates/runtime/contracts/component-contracts.runtime-labels.test.ts +7 -1
- package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +4 -1
- package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +4 -1
- package/templates/runtime/contracts/component-contracts.runtime.ts +15 -10
- package/templates/runtime/contracts/component-contracts.test.ts +2 -0
- package/templates/runtime/contracts/decision-contracts.test.ts +43 -4
- package/templates/runtime/contracts/decision-contracts.ts +10 -10
- package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
- package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
- package/templates/runtime/export/artifact-download.test.ts +71 -0
- package/templates/runtime/export/artifact-download.ts +80 -0
- package/templates/runtime/export/artifact-export-request.ts +23 -0
- package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
- package/templates/runtime/export/artifact-export-settings.ts +96 -0
- package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
- package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
- package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
- package/templates/runtime/export/artifact-frame-state.ts +44 -0
- package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
- package/templates/runtime/export/artifact-scene-frame.ts +118 -0
- package/templates/runtime/export/export-background.ts +46 -0
- package/templates/runtime/export/export-error.ts +43 -0
- package/templates/runtime/export/export.test.ts +49 -226
- package/templates/runtime/export/image-artifact-export.test.ts +111 -0
- package/templates/runtime/export/image-artifact-export.ts +145 -0
- package/templates/runtime/export/index.ts +44 -3
- package/templates/runtime/export/product-export-renderer.test.ts +77 -0
- package/templates/runtime/export/product-export-renderer.ts +63 -0
- package/templates/runtime/export/video-artifact-export.test.ts +166 -0
- package/templates/runtime/export/video-artifact-export.ts +164 -0
- package/templates/runtime/export/video-encoding-backend.ts +124 -0
- package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
- package/templates/runtime/export/video-encoding-policy.ts +86 -0
- package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
- package/templates/runtime/export/video-frame-schedule.ts +42 -0
- package/templates/runtime/index.ts +3 -0
- package/templates/runtime/model-import/model-import-limits.ts +15 -4
- package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
- package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
- package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +15 -304
- package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +41 -27
- package/templates/runtime/react/app-shell/toolcraft-app.tsx +39 -11
- package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
- package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
- package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
- package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
- package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
- package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
- package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +76 -0
- package/templates/runtime/react/canvas/canvas-shell-test-utils.tsx +13 -1
- package/templates/runtime/react/canvas/canvas-shell.tsx +16 -1
- package/templates/runtime/react/canvas/product-scene-surface.test.tsx +195 -0
- package/templates/runtime/react/canvas/product-scene-surface.tsx +155 -0
- package/templates/runtime/react/canvas/runtime-scene-export.ts +1 -1
- package/templates/runtime/react/controls-panel/__tests__/control-conditions.test.ts +61 -0
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.collection-actions.test.tsx +89 -0
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.control-visibility.test.tsx +40 -19
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +18 -84
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-collection.test.tsx +62 -0
- package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-color-bank.test.tsx +67 -0
- package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +43 -87
- package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +68 -0
- package/templates/runtime/react/controls-panel/conditions/control-conditions.ts +32 -119
- package/templates/runtime/react/controls-panel/layout/controls-panel-control-group.tsx +5 -2
- package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +10 -8
- package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +3 -3
- package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.test.tsx +63 -0
- package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.tsx +138 -0
- package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-items.tsx +104 -0
- package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-renderer.tsx +115 -236
- package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.test.tsx +95 -0
- package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.tsx +82 -0
- package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.test.tsx +73 -1
- package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.tsx +50 -1
- package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.test.ts +1 -1
- package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.ts +2 -1
- package/templates/runtime/react/controls-panel/values/controls-panel-collection-values.ts +38 -6
- package/templates/runtime/react/controls-panel/values/controls-panel-value-labels.ts +1 -0
- package/templates/runtime/react/index.ts +4 -0
- package/templates/runtime/react/model-rendering/model-export.ts +1 -1
- package/templates/runtime/react/orientation-gizmo/orientation-gizmo-selection.ts +5 -5
- package/templates/runtime/react/orientation-gizmo/use-toolcraft-orientation-control-selection.ts +2 -2
- package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
- package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
- package/templates/runtime/scene/scene-bounds.test.ts +23 -0
- package/templates/runtime/scene/scene-bounds.ts +32 -19
- package/templates/runtime/schema/app-capabilities.test.ts +4 -0
- package/templates/runtime/schema/canvas-render-scale.ts +62 -0
- package/templates/runtime/schema/collection-item-controls.ts +29 -0
- package/templates/runtime/schema/control-applicability.test.ts +127 -0
- package/templates/runtime/schema/control-applicability.ts +433 -0
- package/templates/runtime/schema/control-schema-normalization.ts +177 -11
- package/templates/runtime/schema/controls-panel-actions.ts +2 -2
- package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
- package/templates/runtime/schema/controls-panel-section-layout.ts +14 -9
- package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +62 -3
- package/templates/runtime/schema/define-toolcraft.collection-actions-item-controls.test.ts +127 -0
- package/templates/runtime/schema/define-toolcraft.control-sections.test.ts +10 -4
- package/templates/runtime/schema/define-toolcraft.file-drop-item-controls.test.ts +98 -0
- package/templates/runtime/schema/define-toolcraft.file-drop-normalization.test.ts +27 -0
- package/templates/runtime/schema/define-toolcraft.segmented-control-fit.test.ts +20 -2
- package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
- package/templates/runtime/schema/define-toolcraft.setup-canvas.test.ts +16 -3
- package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
- package/templates/runtime/schema/define-toolcraft.source-collection.test.ts +79 -0
- package/templates/runtime/schema/define-toolcraft.ts +1 -1
- package/templates/runtime/schema/runtime-setup-section.ts +24 -5
- package/templates/runtime/schema/schema-resolvers.ts +0 -56
- package/templates/runtime/schema/types.ts +72 -17
- package/templates/runtime/state/canvas-background-state.ts +6 -0
- package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
- package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
- package/templates/starter/AGENTS.md +18 -15
- package/templates/starter/NOTICE.md +6 -0
- package/templates/starter/docs/toolcraft/README.md +2 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +23 -35
- package/templates/starter/docs/toolcraft/agent-worklog.md +18 -3
- package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -5
- package/templates/starter/docs/toolcraft/component-rules.md +8 -6
- package/templates/starter/docs/toolcraft/core/control-selection.md +9 -3
- package/templates/starter/docs/toolcraft/core/layout.md +7 -7
- package/templates/starter/docs/toolcraft/core/media-upload.md +4 -2
- package/templates/starter/docs/toolcraft/core/performance.md +14 -3
- package/templates/starter/docs/toolcraft/core/runtime-boundary.md +16 -3
- package/templates/starter/docs/toolcraft/core/setup-export.md +49 -19
- package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
- package/templates/starter/docs/toolcraft/custom-controls.md +6 -4
- package/templates/starter/docs/toolcraft/decision-contract.md +3 -1
- package/templates/starter/docs/toolcraft/performance.md +20 -6
- package/templates/starter/docs/toolcraft/renderer-technique.md +5 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +23 -25
- package/templates/starter/docs/toolcraft/workflow.md +7 -2
- package/templates/starter/e2e/app-browser-control-applicability-requirements.spec.ts +163 -0
- package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
- package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +50 -3
- package/templates/starter/e2e/app-browser-orientation-evidence.spec.ts +174 -21
- package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
- package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
- package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
- package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
- package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +5 -34
- package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +61 -16
- package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
- package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
- package/templates/starter/e2e/app-performance.spec.ts +16 -9
- package/templates/starter/e2e/{browser-conditional-output-evidence-helpers.ts → browser-background-output-evidence.ts} +0 -68
- package/templates/starter/e2e/browser-control-applicability-evidence.ts +114 -0
- package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +29 -341
- package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
- package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
- package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
- package/templates/starter/e2e/browser-orientation-gizmo-evidence-helpers.ts +4 -0
- package/templates/starter/e2e/browser-orientation-gizmo-live-preconditions.ts +86 -0
- package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
- package/templates/starter/e2e/browser-runtime-applicability-requirements.ts +84 -0
- package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +13 -3
- package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +72 -35
- package/templates/starter/e2e/browser-semantic-evidence-test-helpers.ts +25 -0
- package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
- package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
- package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
- package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
- package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
- package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
- package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
- package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
- package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
- package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
- package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
- package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
- package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
- package/templates/starter/e2e/performance-path-helpers.ts +210 -91
- package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
- package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
- package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
- package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
- package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
- package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +1 -0
- package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
- package/templates/starter/scripts/toolcraft-contract-manifest.json +1 -0
- package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
- package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
- package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
- package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +95 -103
- package/templates/starter/scripts/toolcraft-product-boundary-module-policy.mjs +101 -0
- package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
- package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +0 -26
- package/templates/starter/scripts/toolcraft-product-control-bindings.mjs +182 -0
- package/templates/starter/scripts/toolcraft-product-control-boundary.mjs +146 -0
- package/templates/starter/scripts/toolcraft-product-control-boundary.test.mjs +186 -0
- package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
- package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
- package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +157 -0
- package/templates/starter/scripts/toolcraft-product-export-boundary.test.mjs +196 -0
- package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
- package/templates/starter/scripts/toolcraft-typescript-source-evidence-structure.test.mjs +20 -0
- package/templates/starter/scripts/toolcraft-typescript-source-evidence.mjs +1 -0
- package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
- package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +157 -206
- package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
- package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
- package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
- package/templates/starter/src/app/acceptance/artifact-export-intent.ts +130 -0
- package/templates/starter/src/app/acceptance/conditions.ts +1 -1
- package/templates/starter/src/app/acceptance/control-acceptance-context.ts +2 -2
- package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +6 -20
- package/templates/starter/src/app/acceptance/control-acceptance-kind-rules.ts +24 -5
- package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +2 -13
- package/templates/starter/src/app/acceptance/control-applicability-cases.test.ts +321 -0
- package/templates/starter/src/app/acceptance/control-applicability-cases.ts +290 -0
- package/templates/starter/src/app/acceptance/control-applicability.ts +332 -0
- package/templates/starter/src/app/acceptance/control-layout-dependency-rules.ts +20 -9
- package/templates/starter/src/app/acceptance/control-layout-entity-rules.ts +4 -6
- package/templates/starter/src/app/acceptance/control-layout-model.ts +4 -4
- package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +3 -10
- package/templates/starter/src/app/acceptance/control-parts.ts +8 -0
- package/templates/starter/src/app/acceptance/control-section-entity-cohesion.test.ts +215 -0
- package/templates/starter/src/app/acceptance/control-section-entity-cohesion.ts +132 -0
- package/templates/starter/src/app/acceptance/control-section-inventory.test.ts +87 -0
- package/templates/starter/src/app/acceptance/control-section-inventory.ts +74 -44
- package/templates/starter/src/app/acceptance/custom-controls.ts +11 -3
- package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
- package/templates/starter/src/app/acceptance/inline-layout.ts +64 -9
- package/templates/starter/src/app/acceptance/media-upload.ts +5 -1
- package/templates/starter/src/app/acceptance/orientation-gizmo.ts +18 -11
- package/templates/starter/src/app/acceptance/output-background-rules.ts +2 -2
- package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +18 -10
- package/templates/starter/src/app/acceptance/output-export-model.ts +54 -11
- package/templates/starter/src/app/acceptance/output-export.ts +35 -12
- package/templates/starter/src/app/acceptance/output-video-export-rules.ts +99 -0
- package/templates/starter/src/app/acceptance/types.ts +30 -8
- package/templates/starter/src/app/acceptance/validate-coverage.ts +20 -2
- package/templates/starter/src/app/starter-acceptance.artifact-export-intent.test.ts +239 -0
- package/templates/starter/src/app/starter-acceptance.background-export.test.ts +33 -0
- package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +16 -1
- package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +65 -0
- package/templates/starter/src/app/starter-acceptance.contract-fixtures.test.ts +41 -0
- package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +112 -16
- package/templates/starter/src/app/starter-acceptance.control-applicability.test.ts +224 -0
- package/templates/starter/src/app/starter-acceptance.control-order.test.ts +58 -3
- package/templates/starter/src/app/starter-acceptance.control-state.test.ts +0 -111
- package/templates/starter/src/app/starter-acceptance.custom-control-collections.test.ts +3 -1
- package/templates/starter/src/app/starter-acceptance.custom-control-valid.test.ts +1 -1
- package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
- package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +22 -0
- package/templates/starter/src/app/{starter-acceptance.image-video-export-settings.test.ts → starter-acceptance.image-export-settings.test.ts} +37 -9
- package/templates/starter/src/app/starter-acceptance.interaction-ownership.test.ts +12 -0
- package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
- package/templates/starter/src/app/starter-acceptance.media-upload.fixtures.ts +11 -1
- package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +28 -0
- package/templates/starter/src/app/starter-acceptance.output-export-intent.test.ts +384 -0
- package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +46 -1
- package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +112 -105
- package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +12 -0
- package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +1 -1
- package/templates/starter/src/app/starter-acceptance.section-dependencies.test.ts +35 -24
- package/templates/starter/src/app/starter-acceptance.section-size.test.ts +72 -0
- package/templates/starter/src/app/starter-acceptance.ts +13 -2
- package/templates/starter/src/app/starter-acceptance.video-export-settings.test.ts +336 -0
- package/templates/starter/src/app/starter-acceptance.view-interaction.test.ts +8 -0
- package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
- package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
- package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
- package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +4 -2
- package/templates/starter/src/routes/index.tsx +2 -0
- package/templates/ui/components/composites/tabs.tsx +2 -2
- package/templates/ui/components/controls/collection-actions/collection-item-groups.test.tsx +38 -0
- package/templates/ui/components/controls/collection-actions/collection-item-groups.tsx +36 -0
- package/templates/ui/components/controls/collection-actions/index.ts +2 -0
- package/templates/ui/components/controls/file-drop/file-drop-collection-control.tsx +137 -0
- package/templates/ui/components/controls/file-drop/file-drop-control.test.tsx +189 -0
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +81 -88
- package/templates/ui/components/controls/file-drop/file-drop-presentation.tsx +131 -9
- package/templates/ui/components/controls/file-drop/file-drop-types.ts +36 -22
- package/templates/ui/components/controls/file-drop/index.ts +1 -0
- package/templates/ui/components/controls/index.ts +3 -0
- package/templates/ui/components/controls/range-slider/range-slider-control.tsx +1 -1
- package/templates/ui/components/controls/range-slider/range-slider-value.test.ts +14 -0
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -2
- package/templates/runtime/export/export.ts +0 -166
- package/templates/starter/src/app/acceptance/condition-exclusivity.test.ts +0 -60
- package/templates/starter/src/app/acceptance/condition-exclusivity.ts +0 -221
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
assertToolcraftExpectedDecodedPixels,
|
|
5
|
+
observeToolcraftDecodedPixels,
|
|
6
|
+
validateToolcraftExpectedDecodedPixels,
|
|
7
|
+
} from "./decoded-pixel-observation";
|
|
8
|
+
|
|
9
|
+
function createCenteredFixture(): Uint8ClampedArray {
|
|
10
|
+
const pixels = new Uint8ClampedArray(64 * 64 * 4);
|
|
11
|
+
for (let y = 0; y < 64; y += 1) {
|
|
12
|
+
for (let x = 0; x < 64; x += 1) {
|
|
13
|
+
const offset = (y * 64 + x) * 4;
|
|
14
|
+
const product = x >= 16 && x < 48 && y >= 16 && y < 48;
|
|
15
|
+
pixels.set(product ? [255, 0, 0, 255] : [0, 0, 0, 255], offset);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return pixels;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("decoded pixels produce stable semantic bounds and hash", async () => {
|
|
22
|
+
const observation = await observeToolcraftDecodedPixels({
|
|
23
|
+
backgroundRgba: [0, 0, 0, 255],
|
|
24
|
+
pixels: createCenteredFixture(),
|
|
25
|
+
sourceHeight: 64,
|
|
26
|
+
sourceWidth: 64,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(observation.decodedPixelHash).toMatch(/^[a-f0-9]{64}$/u);
|
|
30
|
+
expect(observation.nonBackgroundBounds).toEqual({
|
|
31
|
+
height: 0.5,
|
|
32
|
+
width: 0.5,
|
|
33
|
+
x: 0.25,
|
|
34
|
+
y: 0.25,
|
|
35
|
+
});
|
|
36
|
+
expect(observation.centroid).toEqual({ x: 0.5, y: 0.5 });
|
|
37
|
+
expect(observation.occupiedAreaRatio).toBe(0.25);
|
|
38
|
+
expect(() =>
|
|
39
|
+
assertToolcraftExpectedDecodedPixels({
|
|
40
|
+
expectedPixels: [
|
|
41
|
+
{ rgba: [255, 0, 0, 255], xRatio: 0.5, yRatio: 0.5 },
|
|
42
|
+
],
|
|
43
|
+
mediaType: "image/png",
|
|
44
|
+
observation,
|
|
45
|
+
}),
|
|
46
|
+
).not.toThrow();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("decoded pixel expectations cannot provide empty, invalid, or loosened checks", async () => {
|
|
50
|
+
const observation = await observeToolcraftDecodedPixels({
|
|
51
|
+
backgroundRgba: [0, 0, 0, 255],
|
|
52
|
+
pixels: createCenteredFixture(),
|
|
53
|
+
sourceHeight: 64,
|
|
54
|
+
sourceWidth: 64,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(() => validateToolcraftExpectedDecodedPixels([])).toThrow(
|
|
58
|
+
"at least one exact pixel sample",
|
|
59
|
+
);
|
|
60
|
+
expect(() =>
|
|
61
|
+
validateToolcraftExpectedDecodedPixels([
|
|
62
|
+
{ rgba: [256, 0, 0, 255], xRatio: 2, yRatio: 0 } as never,
|
|
63
|
+
]),
|
|
64
|
+
).toThrow();
|
|
65
|
+
expect(() =>
|
|
66
|
+
assertToolcraftExpectedDecodedPixels({
|
|
67
|
+
expectedPixels: [
|
|
68
|
+
{ rgba: [0, 255, 0, 255], xRatio: 0.5, yRatio: 0.5 },
|
|
69
|
+
],
|
|
70
|
+
mediaType: "image/png",
|
|
71
|
+
observation,
|
|
72
|
+
}),
|
|
73
|
+
).toThrow("fixed tolerance is 0");
|
|
74
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { ToolcraftNormalizedPixelBounds } from "./export-artifact-helpers";
|
|
2
|
+
|
|
3
|
+
export const TOOLCRAFT_DECODED_OBSERVATION_SIZE = 64;
|
|
4
|
+
export const TOOLCRAFT_LOSSLESS_PIXEL_DISTANCE_TOLERANCE = 0;
|
|
5
|
+
export const TOOLCRAFT_LOSSY_PIXEL_DISTANCE_TOLERANCE = 48;
|
|
6
|
+
export const TOOLCRAFT_BACKGROUND_PIXEL_DISTANCE_THRESHOLD = 24;
|
|
7
|
+
|
|
8
|
+
export type ToolcraftExpectedDecodedPixel = Readonly<{
|
|
9
|
+
rgba: readonly [number, number, number, number];
|
|
10
|
+
xRatio: number;
|
|
11
|
+
yRatio: number;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export type ToolcraftExpectedDecodedVideoSample = Readonly<{
|
|
15
|
+
pixels: readonly ToolcraftExpectedDecodedPixel[];
|
|
16
|
+
timeSeconds: number;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export type ToolcraftDecodedPixelObservation = Readonly<{
|
|
20
|
+
centroid: Readonly<{ x: number; y: number }> | null;
|
|
21
|
+
decodedPixelHash: string;
|
|
22
|
+
nonBackgroundBounds: ToolcraftNormalizedPixelBounds | null;
|
|
23
|
+
normalizedPixels: Uint8ClampedArray;
|
|
24
|
+
occupiedAreaRatio: number;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
function assertRgba(
|
|
28
|
+
rgba: readonly number[],
|
|
29
|
+
label: string,
|
|
30
|
+
): asserts rgba is readonly [number, number, number, number] {
|
|
31
|
+
if (
|
|
32
|
+
rgba.length !== 4 ||
|
|
33
|
+
rgba.some(
|
|
34
|
+
(channel) =>
|
|
35
|
+
!Number.isSafeInteger(channel) || channel < 0 || channel > 255,
|
|
36
|
+
)
|
|
37
|
+
) {
|
|
38
|
+
throw new Error(`${label} must contain four integer RGBA channels from 0 to 255.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertRatio(value: number, label: string): void {
|
|
43
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
44
|
+
throw new Error(`${label} must be a finite normalized coordinate from 0 to 1.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validateToolcraftExpectedDecodedPixels(
|
|
49
|
+
pixels: readonly ToolcraftExpectedDecodedPixel[],
|
|
50
|
+
label = "Decoded pixel expectations",
|
|
51
|
+
): void {
|
|
52
|
+
if (!Array.isArray(pixels) || pixels.length === 0) {
|
|
53
|
+
throw new Error(`${label} must contain at least one exact pixel sample.`);
|
|
54
|
+
}
|
|
55
|
+
for (const [index, pixel] of pixels.entries()) {
|
|
56
|
+
assertRatio(pixel.xRatio, `${label}[${index}].xRatio`);
|
|
57
|
+
assertRatio(pixel.yRatio, `${label}[${index}].yRatio`);
|
|
58
|
+
assertRgba(pixel.rgba, `${label}[${index}].rgba`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getToolcraftRgbaDistance(
|
|
63
|
+
left: readonly number[],
|
|
64
|
+
right: readonly number[],
|
|
65
|
+
): number {
|
|
66
|
+
return Math.sqrt(
|
|
67
|
+
left.reduce((sum, channel, index) => {
|
|
68
|
+
const delta = channel - (right[index] ?? 0);
|
|
69
|
+
return sum + delta * delta;
|
|
70
|
+
}, 0),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizePixels({
|
|
75
|
+
pixels,
|
|
76
|
+
sourceHeight,
|
|
77
|
+
sourceWidth,
|
|
78
|
+
}: Readonly<{
|
|
79
|
+
pixels: Uint8ClampedArray;
|
|
80
|
+
sourceHeight: number;
|
|
81
|
+
sourceWidth: number;
|
|
82
|
+
}>): Uint8ClampedArray {
|
|
83
|
+
if (
|
|
84
|
+
!Number.isSafeInteger(sourceWidth) ||
|
|
85
|
+
sourceWidth <= 0 ||
|
|
86
|
+
!Number.isSafeInteger(sourceHeight) ||
|
|
87
|
+
sourceHeight <= 0 ||
|
|
88
|
+
pixels.length !== sourceWidth * sourceHeight * 4
|
|
89
|
+
) {
|
|
90
|
+
throw new Error("Decoded pixel input must match positive source dimensions.");
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
sourceWidth === TOOLCRAFT_DECODED_OBSERVATION_SIZE &&
|
|
94
|
+
sourceHeight === TOOLCRAFT_DECODED_OBSERVATION_SIZE
|
|
95
|
+
) {
|
|
96
|
+
return new Uint8ClampedArray(pixels);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const normalized = new Uint8ClampedArray(
|
|
100
|
+
TOOLCRAFT_DECODED_OBSERVATION_SIZE * TOOLCRAFT_DECODED_OBSERVATION_SIZE * 4,
|
|
101
|
+
);
|
|
102
|
+
for (let y = 0; y < TOOLCRAFT_DECODED_OBSERVATION_SIZE; y += 1) {
|
|
103
|
+
const sourceY = Math.min(
|
|
104
|
+
sourceHeight - 1,
|
|
105
|
+
Math.floor(((y + 0.5) / TOOLCRAFT_DECODED_OBSERVATION_SIZE) * sourceHeight),
|
|
106
|
+
);
|
|
107
|
+
for (let x = 0; x < TOOLCRAFT_DECODED_OBSERVATION_SIZE; x += 1) {
|
|
108
|
+
const sourceX = Math.min(
|
|
109
|
+
sourceWidth - 1,
|
|
110
|
+
Math.floor(((x + 0.5) / TOOLCRAFT_DECODED_OBSERVATION_SIZE) * sourceWidth),
|
|
111
|
+
);
|
|
112
|
+
const sourceOffset = (sourceY * sourceWidth + sourceX) * 4;
|
|
113
|
+
const outputOffset =
|
|
114
|
+
(y * TOOLCRAFT_DECODED_OBSERVATION_SIZE + x) * 4;
|
|
115
|
+
normalized.set(pixels.subarray(sourceOffset, sourceOffset + 4), outputOffset);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function hashPixels(pixels: Uint8ClampedArray): Promise<string> {
|
|
122
|
+
const source = new Uint8Array(
|
|
123
|
+
pixels.buffer,
|
|
124
|
+
pixels.byteOffset,
|
|
125
|
+
pixels.byteLength,
|
|
126
|
+
);
|
|
127
|
+
const hash = await crypto.subtle.digest("SHA-256", source);
|
|
128
|
+
return Array.from(new Uint8Array(hash), (byte) =>
|
|
129
|
+
byte.toString(16).padStart(2, "0"),
|
|
130
|
+
).join("");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function observeToolcraftDecodedPixels({
|
|
134
|
+
backgroundRgba,
|
|
135
|
+
pixels,
|
|
136
|
+
sourceHeight,
|
|
137
|
+
sourceWidth,
|
|
138
|
+
}: Readonly<{
|
|
139
|
+
backgroundRgba: readonly [number, number, number, number];
|
|
140
|
+
pixels: Uint8ClampedArray;
|
|
141
|
+
sourceHeight: number;
|
|
142
|
+
sourceWidth: number;
|
|
143
|
+
}>): Promise<ToolcraftDecodedPixelObservation> {
|
|
144
|
+
assertRgba(backgroundRgba, "backgroundRgba");
|
|
145
|
+
const normalizedPixels = normalizePixels({
|
|
146
|
+
pixels,
|
|
147
|
+
sourceHeight,
|
|
148
|
+
sourceWidth,
|
|
149
|
+
});
|
|
150
|
+
let count = 0;
|
|
151
|
+
let minX = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
|
|
152
|
+
let minY = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
|
|
153
|
+
let maxX = -1;
|
|
154
|
+
let maxY = -1;
|
|
155
|
+
let sumX = 0;
|
|
156
|
+
let sumY = 0;
|
|
157
|
+
|
|
158
|
+
for (let y = 0; y < TOOLCRAFT_DECODED_OBSERVATION_SIZE; y += 1) {
|
|
159
|
+
for (let x = 0; x < TOOLCRAFT_DECODED_OBSERVATION_SIZE; x += 1) {
|
|
160
|
+
const offset = (y * TOOLCRAFT_DECODED_OBSERVATION_SIZE + x) * 4;
|
|
161
|
+
const pixel = normalizedPixels.subarray(offset, offset + 4);
|
|
162
|
+
if (
|
|
163
|
+
getToolcraftRgbaDistance(pixel, backgroundRgba) <=
|
|
164
|
+
TOOLCRAFT_BACKGROUND_PIXEL_DISTANCE_THRESHOLD
|
|
165
|
+
) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
count += 1;
|
|
169
|
+
minX = Math.min(minX, x);
|
|
170
|
+
minY = Math.min(minY, y);
|
|
171
|
+
maxX = Math.max(maxX, x);
|
|
172
|
+
maxY = Math.max(maxY, y);
|
|
173
|
+
sumX += x + 0.5;
|
|
174
|
+
sumY += y + 0.5;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const size = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
|
|
179
|
+
return Object.freeze({
|
|
180
|
+
centroid:
|
|
181
|
+
count === 0
|
|
182
|
+
? null
|
|
183
|
+
: Object.freeze({ x: sumX / count / size, y: sumY / count / size }),
|
|
184
|
+
decodedPixelHash: await hashPixels(normalizedPixels),
|
|
185
|
+
nonBackgroundBounds:
|
|
186
|
+
count === 0
|
|
187
|
+
? null
|
|
188
|
+
: Object.freeze({
|
|
189
|
+
height: (maxY - minY + 1) / size,
|
|
190
|
+
width: (maxX - minX + 1) / size,
|
|
191
|
+
x: minX / size,
|
|
192
|
+
y: minY / size,
|
|
193
|
+
}),
|
|
194
|
+
normalizedPixels,
|
|
195
|
+
occupiedAreaRatio: count / (size * size),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function assertToolcraftExpectedDecodedPixels({
|
|
200
|
+
expectedPixels,
|
|
201
|
+
mediaType,
|
|
202
|
+
observation,
|
|
203
|
+
}: Readonly<{
|
|
204
|
+
expectedPixels: readonly ToolcraftExpectedDecodedPixel[];
|
|
205
|
+
mediaType: "image/jpeg" | "image/png" | "video/mp4" | "video/webm";
|
|
206
|
+
observation: ToolcraftDecodedPixelObservation;
|
|
207
|
+
}>): void {
|
|
208
|
+
validateToolcraftExpectedDecodedPixels(expectedPixels);
|
|
209
|
+
const tolerance =
|
|
210
|
+
mediaType === "image/png"
|
|
211
|
+
? TOOLCRAFT_LOSSLESS_PIXEL_DISTANCE_TOLERANCE
|
|
212
|
+
: TOOLCRAFT_LOSSY_PIXEL_DISTANCE_TOLERANCE;
|
|
213
|
+
const size = TOOLCRAFT_DECODED_OBSERVATION_SIZE;
|
|
214
|
+
|
|
215
|
+
for (const expected of expectedPixels) {
|
|
216
|
+
const x = Math.min(size - 1, Math.floor(expected.xRatio * size));
|
|
217
|
+
const y = Math.min(size - 1, Math.floor(expected.yRatio * size));
|
|
218
|
+
const offset = (y * size + x) * 4;
|
|
219
|
+
const actual = observation.normalizedPixels.subarray(offset, offset + 4);
|
|
220
|
+
const distance = getToolcraftRgbaDistance(actual, expected.rgba);
|
|
221
|
+
if (distance > tolerance) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Decoded pixel at (${expected.xRatio}, ${expected.yRatio}) differs from expected RGBA by ${distance.toFixed(2)}; fixed tolerance is ${tolerance}.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -1,12 +1,52 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type ToolcraftNormalizedPixelBounds = Readonly<{
|
|
2
|
+
height: number;
|
|
3
|
+
width: number;
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
export type ToolcraftBinaryArtifactInspection = Readonly<{
|
|
2
9
|
byteLength: number;
|
|
3
10
|
contentHash?: string;
|
|
4
11
|
durationMs?: number;
|
|
5
12
|
frameCount?: number;
|
|
6
13
|
height?: number;
|
|
14
|
+
kind?: "binary";
|
|
7
15
|
mediaType?: string;
|
|
8
16
|
width?: number;
|
|
9
|
-
}
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export type ToolcraftImageArtifactInspection = Readonly<{
|
|
20
|
+
byteLength: number;
|
|
21
|
+
decodedPixelHash: string;
|
|
22
|
+
height: number;
|
|
23
|
+
kind: "image";
|
|
24
|
+
mediaType: "image/jpeg" | "image/png";
|
|
25
|
+
nonBackgroundBounds: ToolcraftNormalizedPixelBounds | null;
|
|
26
|
+
width: number;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type ToolcraftVideoPacketTiming = Readonly<{
|
|
30
|
+
durationSeconds: number;
|
|
31
|
+
timeSeconds: number;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
export type ToolcraftVideoArtifactInspection = Readonly<{
|
|
35
|
+
byteLength: number;
|
|
36
|
+
durationMs: number;
|
|
37
|
+
frameCount: number;
|
|
38
|
+
height: number;
|
|
39
|
+
kind: "video";
|
|
40
|
+
mediaType: "video/mp4" | "video/webm";
|
|
41
|
+
packetTimings: readonly ToolcraftVideoPacketTiming[];
|
|
42
|
+
samplePixelHashes: readonly string[];
|
|
43
|
+
width: number;
|
|
44
|
+
}>;
|
|
45
|
+
|
|
46
|
+
export type ToolcraftExportArtifactInspection =
|
|
47
|
+
| ToolcraftBinaryArtifactInspection
|
|
48
|
+
| ToolcraftImageArtifactInspection
|
|
49
|
+
| ToolcraftVideoArtifactInspection;
|
|
10
50
|
|
|
11
51
|
export type ToolcraftExportArtifactInspectionResult =
|
|
12
52
|
| ToolcraftExportArtifactInspection
|
|
@@ -33,13 +73,46 @@ function assertPositiveInteger(
|
|
|
33
73
|
field: string,
|
|
34
74
|
requirementId: string,
|
|
35
75
|
): asserts value is number {
|
|
76
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Export requirement "${requirementId}" must report a positive ${field} integer.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateNormalizedPixelBounds(
|
|
84
|
+
value: unknown,
|
|
85
|
+
requirementId: string,
|
|
86
|
+
): void {
|
|
87
|
+
if (value === null) return;
|
|
88
|
+
if (!isRecord(value)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Export requirement "${requirementId}" must report normalized nonBackgroundBounds or null.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const { height, width, x, y } = value;
|
|
94
|
+
for (const [field, coordinate] of Object.entries({ height, width, x, y })) {
|
|
95
|
+
if (
|
|
96
|
+
typeof coordinate !== "number" ||
|
|
97
|
+
!Number.isFinite(coordinate) ||
|
|
98
|
+
coordinate < 0 ||
|
|
99
|
+
coordinate > 1 ||
|
|
100
|
+
((field === "height" || field === "width") && coordinate === 0)
|
|
101
|
+
) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Export requirement "${requirementId}" must report normalized nonBackgroundBounds.${field}.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
36
107
|
if (
|
|
37
|
-
typeof
|
|
38
|
-
|
|
39
|
-
|
|
108
|
+
typeof x === "number" &&
|
|
109
|
+
typeof y === "number" &&
|
|
110
|
+
typeof width === "number" &&
|
|
111
|
+
typeof height === "number" &&
|
|
112
|
+
(x + width > 1 || y + height > 1)
|
|
40
113
|
) {
|
|
41
114
|
throw new Error(
|
|
42
|
-
`Export requirement "${requirementId}" must
|
|
115
|
+
`Export requirement "${requirementId}" nonBackgroundBounds must remain inside the normalized artifact.`,
|
|
43
116
|
);
|
|
44
117
|
}
|
|
45
118
|
}
|
|
@@ -64,25 +137,12 @@ export function assertToolcraftProducedArtifact(
|
|
|
64
137
|
}
|
|
65
138
|
}
|
|
66
139
|
|
|
67
|
-
|
|
68
|
-
|
|
140
|
+
function validateInspection(
|
|
141
|
+
inspection: Record<string, unknown>,
|
|
69
142
|
requirementId: string,
|
|
70
|
-
):
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
throw new Error(
|
|
74
|
-
`Export requirement "${requirementId}" must inspect at least one artifact.`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for (const inspection of observations) {
|
|
79
|
-
if (!isRecord(inspection) || !("byteLength" in inspection)) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`Export requirement "${requirementId}" must return a typed artifact inspection with byteLength.`,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
assertPositiveInteger(inspection.byteLength, "byteLength", requirementId);
|
|
143
|
+
): void {
|
|
144
|
+
assertPositiveInteger(inspection.byteLength, "byteLength", requirementId);
|
|
145
|
+
if (inspection.kind === undefined || inspection.kind === "binary") {
|
|
86
146
|
if (inspection.durationMs !== undefined) {
|
|
87
147
|
assertPositiveFiniteNumber(
|
|
88
148
|
inspection.durationMs,
|
|
@@ -105,6 +165,108 @@ export function validateToolcraftExportArtifactInspection(
|
|
|
105
165
|
);
|
|
106
166
|
}
|
|
107
167
|
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (inspection.kind === "image") {
|
|
172
|
+
assertPositiveInteger(inspection.width, "width", requirementId);
|
|
173
|
+
assertPositiveInteger(inspection.height, "height", requirementId);
|
|
174
|
+
if (
|
|
175
|
+
inspection.mediaType !== "image/png" &&
|
|
176
|
+
inspection.mediaType !== "image/jpeg"
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Export requirement "${requirementId}" must report a supported image mediaType.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
typeof inspection.decodedPixelHash !== "string" ||
|
|
184
|
+
!inspection.decodedPixelHash.trim()
|
|
185
|
+
) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Export requirement "${requirementId}" must hash decoded image pixels.`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
validateNormalizedPixelBounds(
|
|
191
|
+
inspection.nonBackgroundBounds,
|
|
192
|
+
requirementId,
|
|
193
|
+
);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (inspection.kind === "video") {
|
|
198
|
+
assertPositiveInteger(inspection.width, "width", requirementId);
|
|
199
|
+
assertPositiveInteger(inspection.height, "height", requirementId);
|
|
200
|
+
assertPositiveInteger(inspection.frameCount, "frameCount", requirementId);
|
|
201
|
+
assertPositiveFiniteNumber(inspection.durationMs, "durationMs", requirementId);
|
|
202
|
+
if (
|
|
203
|
+
inspection.mediaType !== "video/mp4" &&
|
|
204
|
+
inspection.mediaType !== "video/webm"
|
|
205
|
+
) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Export requirement "${requirementId}" must report a supported video mediaType.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (
|
|
211
|
+
!Array.isArray(inspection.packetTimings) ||
|
|
212
|
+
inspection.packetTimings.length !== inspection.frameCount
|
|
213
|
+
) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Export requirement "${requirementId}" must report every real encoded packet timing.`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
for (const [index, timing] of inspection.packetTimings.entries()) {
|
|
219
|
+
if (
|
|
220
|
+
!isRecord(timing) ||
|
|
221
|
+
typeof timing.timeSeconds !== "number" ||
|
|
222
|
+
!Number.isFinite(timing.timeSeconds) ||
|
|
223
|
+
timing.timeSeconds < 0 ||
|
|
224
|
+
typeof timing.durationSeconds !== "number" ||
|
|
225
|
+
!Number.isFinite(timing.durationSeconds) ||
|
|
226
|
+
timing.durationSeconds <= 0
|
|
227
|
+
) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Export requirement "${requirementId}" must report valid timing for encoded packet ${index}.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (
|
|
234
|
+
!Array.isArray(inspection.samplePixelHashes) ||
|
|
235
|
+
inspection.samplePixelHashes.length === 0 ||
|
|
236
|
+
inspection.samplePixelHashes.some(
|
|
237
|
+
(hash) => typeof hash !== "string" || !hash.trim(),
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Export requirement "${requirementId}" must hash decoded video samples.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
throw new Error(
|
|
248
|
+
`Export requirement "${requirementId}" returned an unknown artifact inspection kind.`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function validateToolcraftExportArtifactInspection(
|
|
253
|
+
observation: unknown,
|
|
254
|
+
requirementId: string,
|
|
255
|
+
): ToolcraftExportArtifactInspectionResult {
|
|
256
|
+
const observations = Array.isArray(observation) ? observation : [observation];
|
|
257
|
+
if (observations.length === 0) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Export requirement "${requirementId}" must inspect at least one artifact.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const inspection of observations) {
|
|
264
|
+
if (!isRecord(inspection) || !("byteLength" in inspection)) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Export requirement "${requirementId}" must return a typed artifact inspection with byteLength.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
validateInspection(inspection, requirementId);
|
|
108
270
|
}
|
|
109
271
|
|
|
110
272
|
return observation as ToolcraftExportArtifactInspectionResult;
|
|
@@ -115,12 +277,31 @@ export function getToolcraftSemanticArtifactSignature(
|
|
|
115
277
|
requirementId: string,
|
|
116
278
|
): string {
|
|
117
279
|
validateToolcraftExportArtifactInspection(inspection, requirementId);
|
|
280
|
+
if (inspection.kind === "image") {
|
|
281
|
+
return JSON.stringify({
|
|
282
|
+
decodedPixelHash: inspection.decodedPixelHash,
|
|
283
|
+
height: inspection.height,
|
|
284
|
+
mediaType: inspection.mediaType,
|
|
285
|
+
nonBackgroundBounds: inspection.nonBackgroundBounds,
|
|
286
|
+
width: inspection.width,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (inspection.kind === "video") {
|
|
290
|
+
return JSON.stringify({
|
|
291
|
+
durationMs: inspection.durationMs,
|
|
292
|
+
frameCount: inspection.frameCount,
|
|
293
|
+
height: inspection.height,
|
|
294
|
+
mediaType: inspection.mediaType,
|
|
295
|
+
packetTimings: inspection.packetTimings,
|
|
296
|
+
samplePixelHashes: inspection.samplePixelHashes,
|
|
297
|
+
width: inspection.width,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
118
300
|
if (!inspection.contentHash?.trim()) {
|
|
119
301
|
throw new Error(
|
|
120
302
|
`Export requirement "${requirementId}" must report contentHash from decoded product output for semantic comparison.`,
|
|
121
303
|
);
|
|
122
304
|
}
|
|
123
|
-
|
|
124
305
|
return JSON.stringify({
|
|
125
306
|
contentHash: inspection.contentHash,
|
|
126
307
|
durationMs: inspection.durationMs,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { expect, test, type Download } from "@playwright/test";
|
|
4
|
+
|
|
5
|
+
import { inspectToolcraftImageDownload } from "./image-artifact-inspection";
|
|
6
|
+
|
|
7
|
+
test("image inspection hashes decoded pixels and reports product bounds", async ({
|
|
8
|
+
page,
|
|
9
|
+
}, testInfo) => {
|
|
10
|
+
await page.goto("/");
|
|
11
|
+
const base64 = await page.evaluate(() => {
|
|
12
|
+
const canvas = document.createElement("canvas");
|
|
13
|
+
canvas.width = 64;
|
|
14
|
+
canvas.height = 32;
|
|
15
|
+
const context = canvas.getContext("2d");
|
|
16
|
+
if (!context) throw new Error("Missing test canvas context.");
|
|
17
|
+
context.fillStyle = "#000000";
|
|
18
|
+
context.fillRect(0, 0, 64, 32);
|
|
19
|
+
context.fillStyle = "#FF0000";
|
|
20
|
+
context.fillRect(16, 8, 32, 16);
|
|
21
|
+
return canvas.toDataURL("image/png").split(",")[1]!;
|
|
22
|
+
});
|
|
23
|
+
const artifactPath = testInfo.outputPath("decoded-fixture.png");
|
|
24
|
+
await fs.writeFile(artifactPath, Buffer.from(base64, "base64"));
|
|
25
|
+
const download = { path: async () => artifactPath } as Download;
|
|
26
|
+
|
|
27
|
+
const result = await inspectToolcraftImageDownload({
|
|
28
|
+
backgroundRgba: [0, 0, 0, 255],
|
|
29
|
+
download,
|
|
30
|
+
page,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result.inspection).toMatchObject({
|
|
34
|
+
height: 32,
|
|
35
|
+
kind: "image",
|
|
36
|
+
mediaType: "image/png",
|
|
37
|
+
nonBackgroundBounds: { height: 0.5, width: 0.5, x: 0.25, y: 0.25 },
|
|
38
|
+
width: 64,
|
|
39
|
+
});
|
|
40
|
+
expect(result.inspection.decodedPixelHash).toMatch(/^[a-f0-9]{64}$/u);
|
|
41
|
+
});
|