@forgeax/engine-ecs 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +278 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__fixtures__/field-shape-fixture.d.ts +16 -0
- package/dist/__fixtures__/field-shape-fixture.d.ts.map +1 -0
- package/dist/__tests__/__fixtures__/inspect-scripts.snapshot.d.ts +3 -0
- package/dist/__tests__/__fixtures__/inspect-scripts.snapshot.d.ts.map +1 -0
- package/dist/__tests__/archetype.unit.test.d.ts +2 -0
- package/dist/__tests__/archetype.unit.test.d.ts.map +1 -0
- package/dist/__tests__/array-handle-element.test-d.d.ts +2 -0
- package/dist/__tests__/array-handle-element.test-d.d.ts.map +1 -0
- package/dist/__tests__/buffer-array-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/buffer-array-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/buffer-field-input-widen.unit.test.d.ts +2 -0
- package/dist/__tests__/buffer-field-input-widen.unit.test.d.ts.map +1 -0
- package/dist/__tests__/buffer-pool.test-d.d.ts +2 -0
- package/dist/__tests__/buffer-pool.test-d.d.ts.map +1 -0
- package/dist/__tests__/change-detection.unit.test.d.ts +2 -0
- package/dist/__tests__/change-detection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/command-buffer.test.d.ts +2 -0
- package/dist/__tests__/command-buffer.test.d.ts.map +1 -0
- package/dist/__tests__/commands-deferred.unit.test.d.ts +2 -0
- package/dist/__tests__/commands-deferred.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-catalog.test.d.ts +2 -0
- package/dist/__tests__/component-catalog.test.d.ts.map +1 -0
- package/dist/__tests__/component-definition-identity.unit.test.d.ts +2 -0
- package/dist/__tests__/component-definition-identity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-nan-preflight.unit.test.d.ts +2 -0
- package/dist/__tests__/component-nan-preflight.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-schema.test-d.d.ts +2 -0
- package/dist/__tests__/component-schema.test-d.d.ts.map +1 -0
- package/dist/__tests__/component-schema.test.d.ts +2 -0
- package/dist/__tests__/component-schema.test.d.ts.map +1 -0
- package/dist/__tests__/define-component-options.test-d.d.ts +2 -0
- package/dist/__tests__/define-component-options.test-d.d.ts.map +1 -0
- package/dist/__tests__/deprecated-vocab-rejection.unit.test.d.ts +2 -0
- package/dist/__tests__/deprecated-vocab-rejection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/despawn-all.unit.test.d.ts +2 -0
- package/dist/__tests__/despawn-all.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-core-reduction.characterization.test.d.ts +2 -0
- package/dist/__tests__/ecs-core-reduction.characterization.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-core-reduction.perf.test.d.ts +2 -0
- package/dist/__tests__/ecs-core-reduction.perf.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-error-code-31.test-d.d.ts +2 -0
- package/dist/__tests__/ecs-error-code-31.test-d.d.ts.map +1 -0
- package/dist/__tests__/entity-disabling.unit.test.d.ts +2 -0
- package/dist/__tests__/entity-disabling.unit.test.d.ts.map +1 -0
- package/dist/__tests__/entity-handle-codec-parity.unit.test.d.ts +2 -0
- package/dist/__tests__/entity-handle-codec-parity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-conflict-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-conflict-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-shared-storage.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-shared-storage.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-portable-value.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-portable-value.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-projection.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-projection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-render-read-lease.test-d.d.ts +2 -0
- package/dist/__tests__/externalization-render-read-lease.test-d.d.ts.map +1 -0
- package/dist/__tests__/externalization-render-read-lease.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-render-read-lease.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-root-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-root-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/field-enum-labels-reflection.test.d.ts +2 -0
- package/dist/__tests__/field-enum-labels-reflection.test.d.ts.map +1 -0
- package/dist/__tests__/field-shape-fixture.test.d.ts +2 -0
- package/dist/__tests__/field-shape-fixture.test.d.ts.map +1 -0
- package/dist/__tests__/field-transient-reflection.test.d.ts +2 -0
- package/dist/__tests__/field-transient-reflection.test.d.ts.map +1 -0
- package/dist/__tests__/field-value-type.test-d.d.ts +2 -0
- package/dist/__tests__/field-value-type.test-d.d.ts.map +1 -0
- package/dist/__tests__/fixed-update.unit.test.d.ts +2 -0
- package/dist/__tests__/fixed-update.unit.test.d.ts.map +1 -0
- package/dist/__tests__/handle.test-d.d.ts +2 -0
- package/dist/__tests__/handle.test-d.d.ts.map +1 -0
- package/dist/__tests__/hierarchy.unit.test.d.ts +2 -0
- package/dist/__tests__/hierarchy.unit.test.d.ts.map +1 -0
- package/dist/__tests__/managed-array-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/managed-array-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/minimal-example.test-d.d.ts +2 -0
- package/dist/__tests__/minimal-example.test-d.d.ts.map +1 -0
- package/dist/__tests__/name-read-types.test-d.d.ts +2 -0
- package/dist/__tests__/name-read-types.test-d.d.ts.map +1 -0
- package/dist/__tests__/plugin-service.test.d.ts +2 -0
- package/dist/__tests__/plugin-service.test.d.ts.map +1 -0
- package/dist/__tests__/query-combinations.unit.test.d.ts +2 -0
- package/dist/__tests__/query-combinations.unit.test.d.ts.map +1 -0
- package/dist/__tests__/query-light-extract-flat-column-ratio.perf.test.d.ts +2 -0
- package/dist/__tests__/query-light-extract-flat-column-ratio.perf.test.d.ts.map +1 -0
- package/dist/__tests__/query-storage-trends.perf.test.d.ts +2 -0
- package/dist/__tests__/query-storage-trends.perf.test.d.ts.map +1 -0
- package/dist/__tests__/query-trs-flat-column-ratio.perf.test.d.ts +2 -0
- package/dist/__tests__/query-trs-flat-column-ratio.perf.test.d.ts.map +1 -0
- package/dist/__tests__/query.unit.test.d.ts +2 -0
- package/dist/__tests__/query.unit.test.d.ts.map +1 -0
- package/dist/__tests__/relationship-index.perf.test.d.ts +2 -0
- package/dist/__tests__/relationship-index.perf.test.d.ts.map +1 -0
- package/dist/__tests__/relationship-index.test.d.ts +2 -0
- package/dist/__tests__/relationship-index.test.d.ts.map +1 -0
- package/dist/__tests__/scalar-field-type-owner.test-d.d.ts +2 -0
- package/dist/__tests__/scalar-field-type-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/schedule-determinism.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-determinism.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-inspection.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-inspection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-scope-error.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-scope-error.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-token.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-token.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schema-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/schema-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/serialization.unit.test.d.ts +2 -0
- package/dist/__tests__/serialization.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shared-kernel-eligibility.test-d.d.ts +2 -0
- package/dist/__tests__/shared-kernel-eligibility.test-d.d.ts.map +1 -0
- package/dist/__tests__/shared-kernel-eligibility.unit.test.d.ts +2 -0
- package/dist/__tests__/shared-kernel-eligibility.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shared-ref-store.unit.test.d.ts +2 -0
- package/dist/__tests__/shared-ref-store.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sparse-tag.unit.test.d.ts +2 -0
- package/dist/__tests__/sparse-tag.unit.test.d.ts.map +1 -0
- package/dist/__tests__/spawn-field-error.test-d.d.ts +2 -0
- package/dist/__tests__/spawn-field-error.test-d.d.ts.map +1 -0
- package/dist/__tests__/stale-error.test-d.d.ts +2 -0
- package/dist/__tests__/stale-error.test-d.d.ts.map +1 -0
- package/dist/__tests__/string-view-symmetry.test-d.d.ts +2 -0
- package/dist/__tests__/string-view-symmetry.test-d.d.ts.map +1 -0
- package/dist/__tests__/string-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/string-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/structure-epoch-boundaries.unit.test.d.ts +2 -0
- package/dist/__tests__/structure-epoch-boundaries.unit.test.d.ts.map +1 -0
- package/dist/__tests__/structure-epoch.unit.test.d.ts +2 -0
- package/dist/__tests__/structure-epoch.unit.test.d.ts.map +1 -0
- package/dist/__tests__/table-archetype-mapping.unit.test.d.ts +2 -0
- package/dist/__tests__/table-archetype-mapping.unit.test.d.ts.map +1 -0
- package/dist/__tests__/time.unit.test.d.ts +2 -0
- package/dist/__tests__/time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/types.test-d.d.ts +2 -0
- package/dist/__tests__/types.test-d.d.ts.map +1 -0
- package/dist/__tests__/unique-ref-store.unit.test.d.ts +2 -0
- package/dist/__tests__/unique-ref-store.unit.test.d.ts.map +1 -0
- package/dist/__tests__/utils/handle-numeric.d.ts +14 -0
- package/dist/__tests__/utils/handle-numeric.d.ts.map +1 -0
- package/dist/__tests__/validation-root-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/validation-root-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/visibility-atomic-write.unit.test.d.ts +2 -0
- package/dist/__tests__/visibility-atomic-write.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-change-journal.unit.test.d.ts +2 -0
- package/dist/__tests__/world-change-journal.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-characterization.unit.test.d.ts +2 -0
- package/dist/__tests__/world-characterization.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-consumer-inference.test-d.d.ts +2 -0
- package/dist/__tests__/world-consumer-inference.test-d.d.ts.map +1 -0
- package/dist/__tests__/world-health.contract.test.d.ts +2 -0
- package/dist/__tests__/world-health.contract.test.d.ts.map +1 -0
- package/dist/__tests__/world-inspection.test.d.ts +2 -0
- package/dist/__tests__/world-inspection.test.d.ts.map +1 -0
- package/dist/__tests__/world-internal-identity.unit.test.d.ts +2 -0
- package/dist/__tests__/world-internal-identity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-managed-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/world-managed-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-scheduling.m4.test.d.ts +2 -0
- package/dist/__tests__/world-scheduling.m4.test.d.ts.map +1 -0
- package/dist/__tests__/world-unique-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/world-unique-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world.test-d.d.ts +2 -0
- package/dist/__tests__/world.test-d.d.ts.map +1 -0
- package/dist/buffer-pool.d.ts +109 -0
- package/dist/buffer-pool.d.ts.map +1 -0
- package/dist/commands.d.ts +93 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/component-default-fallback.d.ts +88 -0
- package/dist/component-default-fallback.d.ts.map +1 -0
- package/dist/component-schema.d.ts +34 -0
- package/dist/component-schema.d.ts.map +1 -0
- package/dist/component-value-validate.d.ts +29 -0
- package/dist/component-value-validate.d.ts.map +1 -0
- package/dist/component.d.ts +581 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/entity-handle.d.ts +68 -0
- package/dist/entity-handle.d.ts.map +1 -0
- package/dist/entity.d.ts +86 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/errors/query-and-component-errors.d.ts +132 -0
- package/dist/errors/query-and-component-errors.d.ts.map +1 -0
- package/dist/errors/relationship-errors.d.ts +112 -0
- package/dist/errors/relationship-errors.d.ts.map +1 -0
- package/dist/errors/sprite-and-shared-errors.d.ts +107 -0
- package/dist/errors/sprite-and-shared-errors.d.ts.map +1 -0
- package/dist/errors/validation-errors.d.ts +229 -0
- package/dist/errors/validation-errors.d.ts.map +1 -0
- package/dist/errors.d.ts +763 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/execution/shared-kernel.d.ts +106 -0
- package/dist/execution/shared-kernel.d.ts.map +1 -0
- package/dist/externalization/index.d.ts +31 -0
- package/dist/externalization/index.d.ts.map +1 -0
- package/dist/externalization/index.mjs +298 -0
- package/dist/externalization/index.mjs.map +1 -0
- package/dist/index.d.ts +261 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +7422 -0
- package/dist/index.mjs.map +1 -0
- package/dist/internal.mjs +479 -0
- package/dist/internal.mjs.map +1 -0
- package/dist/plugin-service.d.ts +11 -0
- package/dist/plugin-service.d.ts.map +1 -0
- package/dist/projection/index.d.ts +103 -0
- package/dist/projection/index.d.ts.map +1 -0
- package/dist/projection/index.mjs +615 -0
- package/dist/projection/index.mjs.map +1 -0
- package/dist/query/query.d.ts +58 -0
- package/dist/query/query.d.ts.map +1 -0
- package/dist/relationship-index.d.ts +81 -0
- package/dist/relationship-index.d.ts.map +1 -0
- package/dist/resource.d.ts +70 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/schedule-token.d.ts +16 -0
- package/dist/schedule-token.d.ts.map +1 -0
- package/dist/schedule.d.ts +271 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/shared-ref-store.d.ts +144 -0
- package/dist/shared-ref-store.d.ts.map +1 -0
- package/dist/shared.d.ts +3 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.mjs +364 -0
- package/dist/shared.mjs.map +1 -0
- package/dist/storage/archetype-graph.d.ts +54 -0
- package/dist/storage/archetype-graph.d.ts.map +1 -0
- package/dist/storage/archetype.d.ts +23 -0
- package/dist/storage/archetype.d.ts.map +1 -0
- package/dist/storage/change-detection.d.ts +40 -0
- package/dist/storage/change-detection.d.ts.map +1 -0
- package/dist/storage/column.d.ts +111 -0
- package/dist/storage/column.d.ts.map +1 -0
- package/dist/storage/table.d.ts +29 -0
- package/dist/storage/table.d.ts.map +1 -0
- package/dist/time.d.ts +57 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/unique-ref-store.d.ts +105 -0
- package/dist/unique-ref-store.d.ts.map +1 -0
- package/dist/world-change-journal.d.ts +34 -0
- package/dist/world-change-journal.d.ts.map +1 -0
- package/dist/world-component-access.d.ts +265 -0
- package/dist/world-component-access.d.ts.map +1 -0
- package/dist/world-component-storage.d.ts +282 -0
- package/dist/world-component-storage.d.ts.map +1 -0
- package/dist/world-core.d.ts +39 -0
- package/dist/world-core.d.ts.map +1 -0
- package/dist/world-entity-lifecycle.d.ts +30 -0
- package/dist/world-entity-lifecycle.d.ts.map +1 -0
- package/dist/world-internal.d.ts +14 -0
- package/dist/world-internal.d.ts.map +1 -0
- package/dist/world-scheduling.d.ts +30 -0
- package/dist/world-scheduling.d.ts.map +1 -0
- package/dist/world.d.ts +620 -0
- package/dist/world.d.ts.map +1 -0
- package/package.json +77 -0
- package/src/__fixtures__/field-shape-fixture.ts +30 -0
- package/src/__tests__/__fixtures__/inspect-scripts.snapshot.ts +2 -0
- package/src/__tests__/archetype.unit.test.ts +1837 -0
- package/src/__tests__/array-handle-element.test-d.ts +34 -0
- package/src/__tests__/buffer-array-vocab.test-d.ts +53 -0
- package/src/__tests__/buffer-field-input-widen.unit.test.ts +99 -0
- package/src/__tests__/buffer-pool.test-d.ts +32 -0
- package/src/__tests__/change-detection.unit.test.ts +86 -0
- package/src/__tests__/command-buffer.test.ts +179 -0
- package/src/__tests__/commands-deferred.unit.test.ts +68 -0
- package/src/__tests__/component-catalog.test.ts +59 -0
- package/src/__tests__/component-definition-identity.unit.test.ts +24 -0
- package/src/__tests__/component-nan-preflight.unit.test.ts +186 -0
- package/src/__tests__/component-schema.test-d.ts +119 -0
- package/src/__tests__/component-schema.test.ts +55 -0
- package/src/__tests__/define-component-options.test-d.ts +35 -0
- package/src/__tests__/deprecated-vocab-rejection.unit.test.ts +59 -0
- package/src/__tests__/despawn-all.unit.test.ts +17 -0
- package/src/__tests__/ecs-core-reduction.characterization.test.ts +130 -0
- package/src/__tests__/ecs-core-reduction.perf.test.ts +260 -0
- package/src/__tests__/ecs-error-code-31.test-d.ts +42 -0
- package/src/__tests__/entity-disabling.unit.test.ts +37 -0
- package/src/__tests__/entity-handle-codec-parity.unit.test.ts +126 -0
- package/src/__tests__/errors.unit.test.ts +1456 -0
- package/src/__tests__/execution-conflict-boundary.unit.test.ts +123 -0
- package/src/__tests__/execution-shared-storage.unit.test.ts +40 -0
- package/src/__tests__/externalization-portable-value.unit.test.ts +275 -0
- package/src/__tests__/externalization-projection.unit.test.ts +325 -0
- package/src/__tests__/externalization-render-read-lease.test-d.ts +30 -0
- package/src/__tests__/externalization-render-read-lease.unit.test.ts +79 -0
- package/src/__tests__/externalization-root-surface.unit.test.ts +33 -0
- package/src/__tests__/field-enum-labels-reflection.test.ts +66 -0
- package/src/__tests__/field-shape-fixture.test.ts +61 -0
- package/src/__tests__/field-transient-reflection.test.ts +59 -0
- package/src/__tests__/field-value-type.test-d.ts +57 -0
- package/src/__tests__/fixed-update.unit.test.ts +78 -0
- package/src/__tests__/handle.test-d.ts +86 -0
- package/src/__tests__/hierarchy.unit.test.ts +2534 -0
- package/src/__tests__/managed-array-vocab.test-d.ts +137 -0
- package/src/__tests__/minimal-example.test-d.ts +42 -0
- package/src/__tests__/name-read-types.test-d.ts +72 -0
- package/src/__tests__/plugin-service.test.ts +13 -0
- package/src/__tests__/query-combinations.unit.test.ts +42 -0
- package/src/__tests__/query-light-extract-flat-column-ratio.perf.test.ts +252 -0
- package/src/__tests__/query-storage-trends.perf.test.ts +104 -0
- package/src/__tests__/query-trs-flat-column-ratio.perf.test.ts +194 -0
- package/src/__tests__/query.unit.test.ts +196 -0
- package/src/__tests__/relationship-index.perf.test.ts +126 -0
- package/src/__tests__/relationship-index.test.ts +70 -0
- package/src/__tests__/scalar-field-type-owner.test-d.ts +71 -0
- package/src/__tests__/schedule-determinism.unit.test.ts +36 -0
- package/src/__tests__/schedule-inspection.unit.test.ts +81 -0
- package/src/__tests__/schedule-scope-error.unit.test.ts +63 -0
- package/src/__tests__/schedule-token.unit.test.ts +54 -0
- package/src/__tests__/schema-vocab.test-d.ts +103 -0
- package/src/__tests__/serialization.unit.test.ts +2740 -0
- package/src/__tests__/shared-kernel-eligibility.test-d.ts +18 -0
- package/src/__tests__/shared-kernel-eligibility.unit.test.ts +78 -0
- package/src/__tests__/shared-ref-store.unit.test.ts +646 -0
- package/src/__tests__/sparse-tag.unit.test.ts +201 -0
- package/src/__tests__/spawn-field-error.test-d.ts +41 -0
- package/src/__tests__/stale-error.test-d.ts +208 -0
- package/src/__tests__/string-view-symmetry.test-d.ts +26 -0
- package/src/__tests__/string-vocab.test-d.ts +49 -0
- package/src/__tests__/structure-epoch-boundaries.unit.test.ts +39 -0
- package/src/__tests__/structure-epoch.unit.test.ts +48 -0
- package/src/__tests__/table-archetype-mapping.unit.test.ts +78 -0
- package/src/__tests__/time.unit.test.ts +79 -0
- package/src/__tests__/types.test-d.ts +38 -0
- package/src/__tests__/unique-ref-store.unit.test.ts +348 -0
- package/src/__tests__/utils/handle-numeric.ts +30 -0
- package/src/__tests__/validation-root-surface.unit.test.ts +15 -0
- package/src/__tests__/visibility-atomic-write.unit.test.ts +82 -0
- package/src/__tests__/world-change-journal.unit.test.ts +97 -0
- package/src/__tests__/world-characterization.unit.test.ts +408 -0
- package/src/__tests__/world-consumer-inference.test-d.ts +16 -0
- package/src/__tests__/world-health.contract.test.ts +87 -0
- package/src/__tests__/world-inspection.test.ts +37 -0
- package/src/__tests__/world-internal-identity.unit.test.ts +17 -0
- package/src/__tests__/world-managed-roundtrip.unit.test.ts +98 -0
- package/src/__tests__/world-scheduling.m4.test.ts +52 -0
- package/src/__tests__/world-unique-roundtrip.unit.test.ts +357 -0
- package/src/__tests__/world.test-d.ts +156 -0
- package/src/buffer-pool.ts +357 -0
- package/src/commands.ts +644 -0
- package/src/component-default-fallback.ts +236 -0
- package/src/component-schema.ts +92 -0
- package/src/component-value-validate.ts +135 -0
- package/src/component.ts +1268 -0
- package/src/entity-handle.ts +104 -0
- package/src/entity.ts +140 -0
- package/src/errors/query-and-component-errors.ts +200 -0
- package/src/errors/relationship-errors.ts +198 -0
- package/src/errors/sprite-and-shared-errors.ts +231 -0
- package/src/errors/validation-errors.ts +506 -0
- package/src/errors.ts +1464 -0
- package/src/execution/shared-kernel.ts +392 -0
- package/src/externalization/index.ts +170 -0
- package/src/index.ts +414 -0
- package/src/plugin-service.ts +35 -0
- package/src/projection/index.ts +318 -0
- package/src/query/query.ts +606 -0
- package/src/relationship-index.ts +175 -0
- package/src/resource.ts +106 -0
- package/src/schedule-token.ts +40 -0
- package/src/schedule.ts +846 -0
- package/src/shared-ref-store.ts +418 -0
- package/src/shared.ts +23 -0
- package/src/storage/archetype-graph.ts +166 -0
- package/src/storage/archetype.ts +78 -0
- package/src/storage/change-detection.ts +231 -0
- package/src/storage/column.ts +247 -0
- package/src/storage/table.ts +153 -0
- package/src/time.ts +122 -0
- package/src/unique-ref-store.ts +278 -0
- package/src/world-change-journal.ts +84 -0
- package/src/world-component-access.ts +1421 -0
- package/src/world-component-storage.ts +1178 -0
- package/src/world-core.ts +74 -0
- package/src/world-entity-lifecycle.ts +539 -0
- package/src/world-internal.ts +75 -0
- package/src/world-scheduling.ts +499 -0
- package/src/world.ts +1399 -0
|
@@ -0,0 +1,1456 @@
|
|
|
1
|
+
// Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
|
|
2
|
+
// biome-ignore-all lint/complexity/noUselessLoneBlockStatements: scope isolation between merged source files
|
|
3
|
+
//
|
|
4
|
+
// Source files (N=10):
|
|
5
|
+
// - packages/ecs/src/__tests__/ac16-register-component-grep-gate.test.ts
|
|
6
|
+
// - packages/ecs/src/__tests__/buffer-array-errors.test.ts
|
|
7
|
+
// - packages/ecs/src/__tests__/component-global-index.test.ts
|
|
8
|
+
// - packages/ecs/src/__tests__/component-schema-json.test.ts
|
|
9
|
+
// - packages/ecs/src/__tests__/errors.test.ts
|
|
10
|
+
// - packages/ecs/src/__tests__/managed-entity-dangling-errors.test.ts
|
|
11
|
+
// - packages/ecs/src/__tests__/relationship-errors.test.ts
|
|
12
|
+
// - packages/ecs/src/__tests__/result.test.ts
|
|
13
|
+
// - packages/ecs/src/__tests__/spawn-light-invalid-bounds-error-code.test.ts
|
|
14
|
+
// - packages/ecs/src/__tests__/sprite-animation-invalid-error.test.ts
|
|
15
|
+
//
|
|
16
|
+
// Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
|
|
17
|
+
// source as ancestorTitles[0]. Top-level imports merged + deduped.
|
|
18
|
+
|
|
19
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
20
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { dirname, resolve } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
24
|
+
import { describe, expect, expectTypeOf, it, test } from 'vitest';
|
|
25
|
+
import { componentSchema, defineComponent } from '../component';
|
|
26
|
+
import type { EcsErrorCode } from '../errors';
|
|
27
|
+
import {
|
|
28
|
+
ComponentAlreadyPresentError,
|
|
29
|
+
ComponentNotPresentError,
|
|
30
|
+
CyclicDependencyError,
|
|
31
|
+
EntityIndexOverflowError,
|
|
32
|
+
FixedSizeMismatchError,
|
|
33
|
+
ManagedBufferOutOfBoundsError,
|
|
34
|
+
ManagedBufferShrinkNotSupportedError,
|
|
35
|
+
RelationshipDetachMismatchError,
|
|
36
|
+
RelationshipMirrorComponentNotRegisteredError,
|
|
37
|
+
RelationshipMirrorFieldTypeMismatchError,
|
|
38
|
+
RelationshipSelfCycleError,
|
|
39
|
+
ResourceNotFoundError,
|
|
40
|
+
SchemaUnsupportedFieldError,
|
|
41
|
+
SpriteAnimationInvalidError,
|
|
42
|
+
SpriteInstancesCountMismatchError,
|
|
43
|
+
SpriteInstancesMutuallyExclusiveWithInstancesError,
|
|
44
|
+
SpriteInstancesRequiresSpriteShaderError,
|
|
45
|
+
StaleEntityError,
|
|
46
|
+
UniqueRefDoubleReleaseError,
|
|
47
|
+
UniqueRefReleasedError,
|
|
48
|
+
} from '../errors';
|
|
49
|
+
import { World } from '../world';
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
// ─── from ac16-register-component-grep-gate.test.ts ───
|
|
53
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
const repoRoot = resolve(here, '..', '..', '..', '..');
|
|
55
|
+
|
|
56
|
+
const SCAN_DIRS = ['packages', 'apps', 'templates'];
|
|
57
|
+
|
|
58
|
+
const NAME = 'register' + 'Component';
|
|
59
|
+
const CALL_PATTERN = `[.](${NAME}|${NAME}Checked)[(]`;
|
|
60
|
+
const FIELD_PATTERN = `${NAME}:`;
|
|
61
|
+
|
|
62
|
+
function sourceHits(pattern: string): string[] {
|
|
63
|
+
const expression = new RegExp(pattern);
|
|
64
|
+
const hits: string[] = [];
|
|
65
|
+
const visit = (relativeDir: string): void => {
|
|
66
|
+
for (const entry of readdirSync(resolve(repoRoot, relativeDir), { withFileTypes: true })) {
|
|
67
|
+
const relativePath = `${relativeDir}/${entry.name}`;
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
if (entry.name !== 'dist' && entry.name !== 'node_modules') visit(relativePath);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!entry.isFile() || (!entry.name.endsWith('.ts') && !entry.name.endsWith('.mjs'))) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
for (const [index, line] of readFileSync(resolve(repoRoot, relativePath), 'utf8')
|
|
76
|
+
.split('\n')
|
|
77
|
+
.entries()) {
|
|
78
|
+
if (expression.test(line)) hits.push(`${relativePath}:${index + 1}:${line.trim()}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
for (const directory of SCAN_DIRS) {
|
|
83
|
+
visit(directory);
|
|
84
|
+
}
|
|
85
|
+
return hits;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe('ac16-register-component-grep-gate.test.ts', () => {
|
|
89
|
+
describe('AC-16 - register-component call surface is zero repo-wide (w20)', () => {
|
|
90
|
+
it('layer 1: zero method-call sites for the deleted register* methods', () => {
|
|
91
|
+
const hits = sourceHits(CALL_PATTERN);
|
|
92
|
+
expect(hits, `unexpected register-component call sites:\n${hits.join('\n')}`).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('layer 2: zero mock interface field declarations for the deleted method', () => {
|
|
96
|
+
const hits = sourceHits(FIELD_PATTERN);
|
|
97
|
+
expect(
|
|
98
|
+
hits,
|
|
99
|
+
`unexpected register-component field declarations:\n${hits.join('\n')}`,
|
|
100
|
+
).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
// ─── from buffer-array-errors.test.ts ───
|
|
108
|
+
describe('buffer-array-errors.test.ts', () => {
|
|
109
|
+
describe('EcsErrorCode — buffer/array vocab collapse delta (w7, AC-07)', () => {
|
|
110
|
+
it('FixedSizeMismatchError carries .code = "fixed-size-mismatch" + detail.expected / detail.actual', () => {
|
|
111
|
+
const err = new FixedSizeMismatchError('meta', 8, 7);
|
|
112
|
+
expect(err.code).toBe('fixed-size-mismatch');
|
|
113
|
+
expect(err.detail).toEqual({ expected: 8, actual: 7 });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('EcsErrorCode keeps only the core fixed-size write failure', () => {
|
|
117
|
+
const codes: EcsErrorCode[] = ['fixed-size-mismatch'];
|
|
118
|
+
let hits = 0;
|
|
119
|
+
for (const code of codes) {
|
|
120
|
+
switch (code) {
|
|
121
|
+
case 'fixed-size-mismatch':
|
|
122
|
+
hits += 1;
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
void code;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
expect(hits).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('owner-specific validation codes stay outside EcsErrorCode', () => {
|
|
132
|
+
// @ts-expect-error render owns this validation code.
|
|
133
|
+
const renderCode: EcsErrorCode = 'instance-transforms-stride-mismatch';
|
|
134
|
+
// @ts-expect-error schema parsing owns this validation code.
|
|
135
|
+
const schemaCode: EcsErrorCode = 'managed-array-element-type-not-allowed';
|
|
136
|
+
expect([renderCode, schemaCode]).toHaveLength(2);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('deleted managed-array-* codes are no longer assignable to EcsErrorCode', () => {
|
|
140
|
+
// @ts-expect-error 'managed-array-index-out-of-bounds' was deleted by feat-20260515 w11.
|
|
141
|
+
const a: EcsErrorCode = 'managed-array-index-out-of-bounds';
|
|
142
|
+
// @ts-expect-error 'managed-array-pop-empty' was deleted by feat-20260515 w11 (renamed to 'array-pop-empty').
|
|
143
|
+
const b: EcsErrorCode = 'managed-array-pop-empty';
|
|
144
|
+
// @ts-expect-error 'managed-array-shrink-not-supported' was deleted by feat-20260515 w11.
|
|
145
|
+
const c: EcsErrorCode = 'managed-array-shrink-not-supported';
|
|
146
|
+
// @ts-expect-error 'managed-array-stride-mismatch' was deleted by feat-20260515 w11 (replaced by 'instance-transforms-stride-mismatch').
|
|
147
|
+
const d: EcsErrorCode = 'managed-array-stride-mismatch';
|
|
148
|
+
void a;
|
|
149
|
+
void b;
|
|
150
|
+
void c;
|
|
151
|
+
void d;
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
{
|
|
158
|
+
// ─── from component-schema-json.test.ts ───
|
|
159
|
+
// NOTE: two source files both defined defineComponent('C', ...) with different schemas.
|
|
160
|
+
// The second defineComponent('C', diff-schema) is renamed to 'C2' to avoid schema-mismatch
|
|
161
|
+
// at merge time (global component registry, same-name diff-schema -> throw).
|
|
162
|
+
|
|
163
|
+
describe('component-schema-json.test.ts', () => {
|
|
164
|
+
describe('owner schema projection — offline manifest discoverability (w29)', () => {
|
|
165
|
+
test('JSON.stringify(componentSchema(C)) includes array<entity> / array<f32, 16> / buffer<16> / buffer literal substrings', () => {
|
|
166
|
+
const Hierarchy = defineComponent('Hierarchy', {
|
|
167
|
+
children: 'array<entity>',
|
|
168
|
+
instanceTransforms: 'array<f32, 16>',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const BufferBag = defineComponent('BufferBag', {
|
|
172
|
+
meta: 'buffer<16>',
|
|
173
|
+
blob: 'buffer',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const json1 = JSON.stringify(componentSchema(Hierarchy));
|
|
177
|
+
const json2 = JSON.stringify(componentSchema(BufferBag));
|
|
178
|
+
|
|
179
|
+
expect(json1).toContain('array<entity>');
|
|
180
|
+
expect(json1).toContain('array<f32, 16>');
|
|
181
|
+
expect(json2).toContain('buffer<16>');
|
|
182
|
+
expect(json2).toContain('"buffer"');
|
|
183
|
+
|
|
184
|
+
expect(JSON.parse(json1)).toEqual({
|
|
185
|
+
children: 'array<entity>',
|
|
186
|
+
instanceTransforms: 'array<f32, 16>',
|
|
187
|
+
});
|
|
188
|
+
expect(JSON.parse(json2)).toEqual({
|
|
189
|
+
meta: 'buffer<16>',
|
|
190
|
+
blob: 'buffer',
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('schema literal types survive the JSON round-trip (no widening to string)', () => {
|
|
195
|
+
const C2 = defineComponent('SchemaJsonC2', {
|
|
196
|
+
tag: 'array<u32>',
|
|
197
|
+
slot: 'buffer<32>',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const tagKeyword: 'array<u32>' = componentSchema(C2).tag;
|
|
201
|
+
const slotKeyword: 'buffer<32>' = componentSchema(C2).slot;
|
|
202
|
+
expect(tagKeyword).toBe('array<u32>');
|
|
203
|
+
expect(slotKeyword).toBe('buffer<32>');
|
|
204
|
+
|
|
205
|
+
expect(JSON.stringify(componentSchema(C2))).toContain('array<u32>');
|
|
206
|
+
expect(JSON.stringify(componentSchema(C2))).toContain('buffer<32>');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('component tokens do not carry a second serialization method', () => {
|
|
210
|
+
const C = defineComponent('C', {
|
|
211
|
+
children: 'array<entity>',
|
|
212
|
+
meta: 'buffer<16>',
|
|
213
|
+
blob: 'buffer',
|
|
214
|
+
});
|
|
215
|
+
const serialized = JSON.stringify(componentSchema(C));
|
|
216
|
+
expect(serialized).toContain('array<entity>');
|
|
217
|
+
expect(serialized).toContain('buffer<16>');
|
|
218
|
+
expect(serialized).toContain('"buffer"');
|
|
219
|
+
expect('toSchemaJSON' in C).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
{
|
|
226
|
+
// ─── from errors.test.ts ───
|
|
227
|
+
describe('errors.test.ts', () => {
|
|
228
|
+
describe('EcsError unified interface — .code property', () => {
|
|
229
|
+
const errorCases: Array<{
|
|
230
|
+
label: string;
|
|
231
|
+
create: () => Error & { hint: string; code: string };
|
|
232
|
+
expectedCode: string;
|
|
233
|
+
}> = [
|
|
234
|
+
{
|
|
235
|
+
label: 'EntityIndexOverflowError',
|
|
236
|
+
create: () => new EntityIndexOverflowError(99999999),
|
|
237
|
+
expectedCode: 'entity-index-overflow',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
label: 'SchemaUnsupportedFieldError',
|
|
241
|
+
create: () => new SchemaUnsupportedFieldError('x', 'vec3'),
|
|
242
|
+
expectedCode: 'schema-unsupported-field',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
label: 'StaleEntityError',
|
|
246
|
+
create: () => new StaleEntityError(100, 5, 3),
|
|
247
|
+
expectedCode: 'stale-entity',
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
label: 'ComponentAlreadyPresentError',
|
|
251
|
+
create: () => new ComponentAlreadyPresentError(42, 'Pos'),
|
|
252
|
+
expectedCode: 'component-already-present',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
label: 'ComponentNotPresentError',
|
|
256
|
+
create: () => new ComponentNotPresentError(42, 'Velocity'),
|
|
257
|
+
expectedCode: 'component-not-present',
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
label: 'CyclicDependencyError',
|
|
261
|
+
create: () => new CyclicDependencyError(['A', 'B', 'A']),
|
|
262
|
+
expectedCode: 'cyclic-dependency',
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
label: 'ResourceNotFoundError',
|
|
266
|
+
create: () => new ResourceNotFoundError('Time'),
|
|
267
|
+
expectedCode: 'resource-not-found',
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
for (const { label, create, expectedCode } of errorCases) {
|
|
272
|
+
describe(label, () => {
|
|
273
|
+
it(`.code equals "${expectedCode}"`, () => {
|
|
274
|
+
const e = create();
|
|
275
|
+
expect(e.code).toBe(expectedCode);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('.code is kebab-case', () => {
|
|
279
|
+
const e = create();
|
|
280
|
+
expect(e.code).toMatch(/^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('.name is readable', () => {
|
|
284
|
+
const e = create();
|
|
285
|
+
expect(typeof e.name).toBe('string');
|
|
286
|
+
expect(e.name.length).toBeGreaterThan(0);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('.message is readable', () => {
|
|
290
|
+
const e = create();
|
|
291
|
+
expect(typeof e.message).toBe('string');
|
|
292
|
+
expect(e.message.length).toBeGreaterThan(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('.hint is readable', () => {
|
|
296
|
+
const e = create();
|
|
297
|
+
expect(typeof e.hint).toBe('string');
|
|
298
|
+
expect(e.hint.length).toBeGreaterThan(0);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('StaleEntityError enhanced fields (F-04)', () => {
|
|
305
|
+
it('has component field (optional string)', () => {
|
|
306
|
+
const e = new StaleEntityError(100, 5, 3, {
|
|
307
|
+
component: 'Position',
|
|
308
|
+
operation: 'get',
|
|
309
|
+
expectedGeneration: 3,
|
|
310
|
+
actualGeneration: 2,
|
|
311
|
+
});
|
|
312
|
+
expect(e.component).toBe('Position');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('component is undefined when not provided', () => {
|
|
316
|
+
const e = new StaleEntityError(100, 5, 3, {
|
|
317
|
+
operation: 'despawn',
|
|
318
|
+
expectedGeneration: 3,
|
|
319
|
+
actualGeneration: 2,
|
|
320
|
+
});
|
|
321
|
+
expect(e.component).toBeUndefined();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('has operation field (string)', () => {
|
|
325
|
+
const e = new StaleEntityError(100, 5, 3, {
|
|
326
|
+
operation: 'get',
|
|
327
|
+
expectedGeneration: 3,
|
|
328
|
+
actualGeneration: 2,
|
|
329
|
+
});
|
|
330
|
+
expect(e.operation).toBe('get');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('has expectedGeneration field (number)', () => {
|
|
334
|
+
const e = new StaleEntityError(100, 5, 3, {
|
|
335
|
+
operation: 'set',
|
|
336
|
+
expectedGeneration: 5,
|
|
337
|
+
actualGeneration: 3,
|
|
338
|
+
});
|
|
339
|
+
expect(e.expectedGeneration).toBe(5);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('has actualGeneration field (number)', () => {
|
|
343
|
+
const e = new StaleEntityError(100, 5, 3, {
|
|
344
|
+
operation: 'set',
|
|
345
|
+
expectedGeneration: 5,
|
|
346
|
+
actualGeneration: 3,
|
|
347
|
+
});
|
|
348
|
+
expect(e.actualGeneration).toBe(3);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('preserves original entity/index/generation fields (backward compat)', () => {
|
|
352
|
+
const e = new StaleEntityError(100, 5, 3, {
|
|
353
|
+
component: 'Velocity',
|
|
354
|
+
operation: 'get',
|
|
355
|
+
expectedGeneration: 3,
|
|
356
|
+
actualGeneration: 1,
|
|
357
|
+
});
|
|
358
|
+
expect(e.name).toBe('StaleEntityError');
|
|
359
|
+
expect(e.message).toBeDefined();
|
|
360
|
+
expect(e.hint).toBeDefined();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('backward compat: constructor without enhanced fields still works', () => {
|
|
364
|
+
const e = new StaleEntityError(100, 5, 3);
|
|
365
|
+
expect(e.name).toBe('StaleEntityError');
|
|
366
|
+
expect(e.hint).toBeDefined();
|
|
367
|
+
expect(e.component).toBeUndefined();
|
|
368
|
+
expect(e.operation).toBeUndefined();
|
|
369
|
+
expect(e.expectedGeneration).toBeUndefined();
|
|
370
|
+
expect(e.actualGeneration).toBeUndefined();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('AC-09 EcsErrorCode net-zero relative fence (w16)', () => {
|
|
375
|
+
function parseEcsErrorCodeMembers(src: string): string[] {
|
|
376
|
+
const header = 'export type EcsErrorCode =';
|
|
377
|
+
const start = src.indexOf(header);
|
|
378
|
+
if (start === -1) return [];
|
|
379
|
+
const bodyStart = start + header.length;
|
|
380
|
+
const endMarker = src.indexOf('export type EcsErrorDetail');
|
|
381
|
+
const searchEnd = endMarker > -1 ? endMarker : src.length;
|
|
382
|
+
let lastSemicolon = -1;
|
|
383
|
+
for (let i = bodyStart; i < searchEnd; i++) {
|
|
384
|
+
if (src[i] === ';') lastSemicolon = i;
|
|
385
|
+
}
|
|
386
|
+
const body = src.slice(bodyStart, lastSemicolon > -1 ? lastSemicolon : searchEnd);
|
|
387
|
+
|
|
388
|
+
const members = new Set<string>();
|
|
389
|
+
for (const raw of body.split('\n')) {
|
|
390
|
+
const line = raw.trim();
|
|
391
|
+
const member = line.match(/^\|\s*'([^']+)'/);
|
|
392
|
+
if (member) {
|
|
393
|
+
members.add(member[1] ?? '');
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const first = line.match(/^=\s*'([^']+)'/);
|
|
397
|
+
if (first) {
|
|
398
|
+
members.add(first[1] ?? '');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return [...members].sort();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
it('EcsErrorCode member set = git HEAD member set (net add 0, net delete 0)', () => {
|
|
405
|
+
const errorsPath = resolve(__dirname, '..', 'errors.ts');
|
|
406
|
+
|
|
407
|
+
const currentSrc = readFileSync(errorsPath, 'utf-8');
|
|
408
|
+
const currentMembers = parseEcsErrorCodeMembers(currentSrc);
|
|
409
|
+
|
|
410
|
+
let headSrc: string;
|
|
411
|
+
try {
|
|
412
|
+
headSrc = execSync('git show HEAD:packages/ecs/src/errors.ts', {
|
|
413
|
+
encoding: 'utf-8',
|
|
414
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
415
|
+
});
|
|
416
|
+
} catch {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const headMembers = parseEcsErrorCodeMembers(headSrc);
|
|
420
|
+
|
|
421
|
+
// M-4 (feat-20260611-ecs-storage-naming-ssot): intentional SCREAMING_SNAKE -> kebab rename.
|
|
422
|
+
// Map head-side SCREAMING_SNAKE codes to their kebab counterparts before comparison.
|
|
423
|
+
const M4_RENAME_MAP: Record<string, string> = {
|
|
424
|
+
ENTITY_INDEX_OVERFLOW: 'entity-index-overflow',
|
|
425
|
+
SCHEMA_UNSUPPORTED_FIELD: 'schema-unsupported-field',
|
|
426
|
+
STALE_ENTITY: 'stale-entity',
|
|
427
|
+
COMPONENT_ALREADY_PRESENT: 'component-already-present',
|
|
428
|
+
COMPONENT_NOT_PRESENT: 'component-not-present',
|
|
429
|
+
CYCLIC_DEPENDENCY: 'cyclic-dependency',
|
|
430
|
+
RESOURCE_NOT_FOUND: 'resource-not-found',
|
|
431
|
+
};
|
|
432
|
+
// feat-20260614-ecs-shared-component-and-unique-rename M2/w5: intentional
|
|
433
|
+
// managed-ref-* -> unique-ref-* error code rename (handle brand
|
|
434
|
+
// `'managed'` -> `'unique'` cascade). Map head-side managed-ref codes
|
|
435
|
+
// to their unique-ref counterparts before comparison.
|
|
436
|
+
const FEAT_20260614_RENAME_MAP: Record<string, string> = {
|
|
437
|
+
'managed-ref-released': 'unique-ref-released',
|
|
438
|
+
'managed-ref-double-release': 'unique-ref-double-release',
|
|
439
|
+
};
|
|
440
|
+
// feat-20260625-sprite-instances-and-tilemap-terrain-static-batch
|
|
441
|
+
// post-merge with PR #520 (transparent collapse): PR 520 narrowed
|
|
442
|
+
// `MaterialSnapshot.shadingModel` to `'unlit' | undefined` — sprite
|
|
443
|
+
// is no longer a shadingModel enum member; identification is via
|
|
444
|
+
// `materialShaderId === 'forgeax::sprite'` (OOS-1 path retained).
|
|
445
|
+
// The guard error code accordingly renames to match the actual
|
|
446
|
+
// discriminator it checks.
|
|
447
|
+
const FEAT_20260625_RENAME_MAP: Record<string, string> = {
|
|
448
|
+
'sprite-instances-requires-sprite-shading-model':
|
|
449
|
+
'sprite-instances-requires-sprite-shader',
|
|
450
|
+
};
|
|
451
|
+
const applyRenames = (m: string): string =>
|
|
452
|
+
FEAT_20260625_RENAME_MAP[m] ?? FEAT_20260614_RENAME_MAP[m] ?? M4_RENAME_MAP[m] ?? m;
|
|
453
|
+
const headSet = new Set(headMembers.map(applyRenames));
|
|
454
|
+
const currentSet = new Set(currentMembers);
|
|
455
|
+
|
|
456
|
+
// bug-20260615 spawn-data-unknown-field-fail-fast: AGENTS.md
|
|
457
|
+
// §Error model evolution permits add-only minor evolution. Each
|
|
458
|
+
// intentional addition lists itself here so accidental drift in
|
|
459
|
+
// unrelated diffs still trips the gate.
|
|
460
|
+
//
|
|
461
|
+
// feat-20260614-ecs-shared-component-and-unique-rename M3 adds the
|
|
462
|
+
// two SharedRefStore error codes (companion to the existing
|
|
463
|
+
// unique-ref-* pair). Intentional add per the same minor-evolution
|
|
464
|
+
// contract.
|
|
465
|
+
//
|
|
466
|
+
// feat-20260623 #511 unified generational handle codec adds the
|
|
467
|
+
// shared-ref-stale + unique-ref-stale pair (gen-mismatch on
|
|
468
|
+
// resolve / retain after the entity+asset shared isRetiredSlot
|
|
469
|
+
// SSOT merge). Intentional add per the same minor-evolution
|
|
470
|
+
// contract.
|
|
471
|
+
// M2 time and scheduling extends the 43-code baseline by exactly three
|
|
472
|
+
// approved codes. The relative fence rejects any additional drift.
|
|
473
|
+
const INTENTIONAL_ADDS = new Set<string>([
|
|
474
|
+
'time-delta-invalid',
|
|
475
|
+
'time-config-invalid',
|
|
476
|
+
'schedule-scope-mismatch',
|
|
477
|
+
'resource-protected',
|
|
478
|
+
'spawn-data-unknown-field',
|
|
479
|
+
'shared-ref-released',
|
|
480
|
+
'shared-ref-double-release',
|
|
481
|
+
'shared-ref-payload-invalid',
|
|
482
|
+
'builtin-slot-not-owned',
|
|
483
|
+
// feat-20260625-sprite-instances-and-tilemap-terrain-static-batch
|
|
484
|
+
// M1 / w2 — 3 declared, fired at render-system-extract entry in
|
|
485
|
+
// M3 w13 (plan-strategy D-6). Minor evolution +3 per AGENTS.md
|
|
486
|
+
// §Error model evolution contract.
|
|
487
|
+
'sprite-instances-count-mismatch',
|
|
488
|
+
'sprite-instances-requires-sprite-shader',
|
|
489
|
+
'sprite-instances-mutually-exclusive-with-instances',
|
|
490
|
+
// feat-20260623-asset-handle-generation M4 — `shared-ref-stale` /
|
|
491
|
+
// `unique-ref-stale` landed on main during this feat's in-flight
|
|
492
|
+
// window (PR #502 + companion lands). Absorbed at merge into
|
|
493
|
+
// INTENTIONAL_ADDS so the net-zero gate stays self-consistent
|
|
494
|
+
// (charter F1: upstream lands must not trigger this feat's fence).
|
|
495
|
+
'shared-ref-stale',
|
|
496
|
+
'unique-ref-stale',
|
|
497
|
+
// feat-20260713-mount-override-component-add-and-shared-ref-round
|
|
498
|
+
// M2 / w9 — P3 shared-field value gate. `shared-field-invalid-value`
|
|
499
|
+
// fails fast when a `shared<T>` / `array<shared<T>>` field is bound to
|
|
500
|
+
// a raw GUID / sidecar object (not a resolved numeric handle) instead
|
|
501
|
+
// of the pre-fix silent zeroing. Minor evolution +1 per AGENTS.md
|
|
502
|
+
// §Error model evolution contract.
|
|
503
|
+
'shared-field-invalid-value',
|
|
504
|
+
// feat-20260714-bevy-style-system-sets M1 / w3 — sole invalid-SystemSet
|
|
505
|
+
// error code. Minor evolution +1 per AGENTS.md §Error model evolution
|
|
506
|
+
// contract.
|
|
507
|
+
'system-set-not-registered',
|
|
508
|
+
'change-epoch-exhausted',
|
|
509
|
+
'query-descriptor-conflict',
|
|
510
|
+
'query-data-requires-fields',
|
|
511
|
+
'query-span-unavailable',
|
|
512
|
+
'query-iteration-invalidated',
|
|
513
|
+
'query-iteration-active',
|
|
514
|
+
'sparse-storage-requires-tag',
|
|
515
|
+
// feat-20260803-entity-visibility M1 — closed enum writes fail
|
|
516
|
+
// before storage mutation. Minor evolution +1 per the same contract.
|
|
517
|
+
'component-field-invalid-value',
|
|
518
|
+
// feat-20260822 ECS core reduction — managed array writes reject
|
|
519
|
+
// arbitrary object payloads before row/buffer mutation.
|
|
520
|
+
'managed-array-invalid-value',
|
|
521
|
+
// feat-20260825-engine-feedback-verifiable-remediation M1 / w4 —
|
|
522
|
+
// object-write numeric preflight rejects NaN before mutation.
|
|
523
|
+
'component-numeric-value-invalid',
|
|
524
|
+
'shared-kernel-ineligible',
|
|
525
|
+
'shared-kernel-failed',
|
|
526
|
+
'world-poisoned',
|
|
527
|
+
'command-failed',
|
|
528
|
+
'system-failed',
|
|
529
|
+
]);
|
|
530
|
+
|
|
531
|
+
const added: string[] = [];
|
|
532
|
+
const deleted: string[] = [];
|
|
533
|
+
const INTENTIONAL_DELETES = new Set<string>([
|
|
534
|
+
'query-descriptor-with-optional-conflict',
|
|
535
|
+
'query-combinations-entity-required',
|
|
536
|
+
// Domain errors now close in render/runtime/scene owner unions;
|
|
537
|
+
// ECS retains their structured detail classes only for migration
|
|
538
|
+
// evidence and no longer counts these codes in EcsErrorCode.
|
|
539
|
+
'component-not-defined',
|
|
540
|
+
'resource-invalid-value',
|
|
541
|
+
'scene-override-type-mismatch',
|
|
542
|
+
'spawn-light-invalid-bounds',
|
|
543
|
+
'sprite-animation-invalid',
|
|
544
|
+
'sprite-instances-count-mismatch',
|
|
545
|
+
'sprite-instances-mutually-exclusive-with-instances',
|
|
546
|
+
'sprite-instances-requires-sprite-shader',
|
|
547
|
+
'entity-index-overflow',
|
|
548
|
+
'schema-unsupported-field',
|
|
549
|
+
'sparse-storage-requires-tag',
|
|
550
|
+
'change-epoch-exhausted',
|
|
551
|
+
'instance-transforms-stride-mismatch',
|
|
552
|
+
'managed-array-element-type-not-allowed',
|
|
553
|
+
'query-data-requires-fields',
|
|
554
|
+
'query-descriptor-conflict',
|
|
555
|
+
'query-iteration-active',
|
|
556
|
+
'query-iteration-invalidated',
|
|
557
|
+
'query-span-unavailable',
|
|
558
|
+
'relationship-mirror-component-not-registered',
|
|
559
|
+
'relationship-mirror-field-type-mismatch',
|
|
560
|
+
'resource-protected',
|
|
561
|
+
'fixed-array-overflow',
|
|
562
|
+
'array-pop-empty',
|
|
563
|
+
'cardinality-exceeded',
|
|
564
|
+
]);
|
|
565
|
+
for (const c of currentSet) {
|
|
566
|
+
if (!headSet.has(c) && !INTENTIONAL_ADDS.has(c)) added.push(c);
|
|
567
|
+
}
|
|
568
|
+
for (const h of headSet) {
|
|
569
|
+
if (!currentSet.has(h) && !INTENTIONAL_DELETES.has(h)) deleted.push(h);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (added.length > 0 || deleted.length > 0) {
|
|
573
|
+
const lines: string[] = [];
|
|
574
|
+
if (added.length > 0)
|
|
575
|
+
lines.push(
|
|
576
|
+
`EcsErrorCode members ADDED by this feat (net +${added.length}): ${added.join(', ')}`,
|
|
577
|
+
);
|
|
578
|
+
if (deleted.length > 0)
|
|
579
|
+
lines.push(
|
|
580
|
+
`EcsErrorCode members DELETED by this feat (net -${deleted.length}): ${deleted.join(', ')}`,
|
|
581
|
+
);
|
|
582
|
+
lines.push(
|
|
583
|
+
`HEAD member count: ${headMembers.length}; current member count: ${currentMembers.length}`,
|
|
584
|
+
);
|
|
585
|
+
throw new Error(lines.join('\n'));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
expect(currentMembers.length).toBeGreaterThan(0);
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
{
|
|
595
|
+
// ─── from managed-entity-dangling-errors.test.ts ───
|
|
596
|
+
describe('managed-entity-dangling-errors.test.ts', () => {
|
|
597
|
+
describe('w5 — managed error classes', () => {
|
|
598
|
+
it('UniqueRefReleasedError carries code + hint + detail.handle', () => {
|
|
599
|
+
const err = new UniqueRefReleasedError(0xdeadbeef, 'MaterialAsset');
|
|
600
|
+
expect(err.code).toBe('unique-ref-released');
|
|
601
|
+
expect(err.hint.length).toBeGreaterThan(0);
|
|
602
|
+
expect(err.detail.handle).toBe(0xdeadbeef);
|
|
603
|
+
expect(err.detail.target).toBe('MaterialAsset');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('UniqueRefDoubleReleaseError carries code + hint + detail.handle', () => {
|
|
607
|
+
const err = new UniqueRefDoubleReleaseError(7, 'MaterialAsset');
|
|
608
|
+
expect(err.code).toBe('unique-ref-double-release');
|
|
609
|
+
expect(err.hint.length).toBeGreaterThan(0);
|
|
610
|
+
expect(err.detail.handle).toBe(7);
|
|
611
|
+
expect(err.detail.target).toBe('MaterialAsset');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('ManagedBufferOutOfBoundsError carries code + hint + detail.{index,size}', () => {
|
|
615
|
+
const err = new ManagedBufferOutOfBoundsError(128, 64);
|
|
616
|
+
expect(err.code).toBe('managed-buffer-out-of-bounds');
|
|
617
|
+
expect(err.hint.length).toBeGreaterThan(0);
|
|
618
|
+
expect(err.detail.index).toBe(128);
|
|
619
|
+
expect(err.detail.size).toBe(64);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('ManagedBufferShrinkNotSupportedError carries code + hint + detail.{requested,current}', () => {
|
|
623
|
+
const err = new ManagedBufferShrinkNotSupportedError(32, 64);
|
|
624
|
+
expect(err.code).toBe('managed-buffer-shrink-not-supported');
|
|
625
|
+
expect(err.hint.length).toBeGreaterThan(0);
|
|
626
|
+
expect(err.detail.requested).toBe(32);
|
|
627
|
+
expect(err.detail.current).toBe(64);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('EcsErrorCode closed union exhaustively covers the managed members', () => {
|
|
631
|
+
function assertNever(_x: never): never {
|
|
632
|
+
throw new Error('unreachable');
|
|
633
|
+
}
|
|
634
|
+
function check(code: EcsErrorCode): string {
|
|
635
|
+
switch (code) {
|
|
636
|
+
case 'stale-entity':
|
|
637
|
+
case 'component-already-present':
|
|
638
|
+
case 'component-not-present':
|
|
639
|
+
case 'cyclic-dependency':
|
|
640
|
+
case 'resource-not-found':
|
|
641
|
+
case 'system-before-unknown':
|
|
642
|
+
case 'unique-ref-released':
|
|
643
|
+
case 'unique-ref-double-release':
|
|
644
|
+
case 'managed-buffer-out-of-bounds':
|
|
645
|
+
case 'managed-buffer-shrink-not-supported':
|
|
646
|
+
case 'fixed-size-mismatch':
|
|
647
|
+
case 'relationship-self-cycle':
|
|
648
|
+
case 'relationship-detach-mismatch':
|
|
649
|
+
case 'remove-essential-component':
|
|
650
|
+
case 'spawn-data-unknown-field':
|
|
651
|
+
case 'shared-ref-released':
|
|
652
|
+
case 'shared-ref-double-release':
|
|
653
|
+
case 'shared-ref-payload-invalid':
|
|
654
|
+
case 'builtin-slot-not-owned':
|
|
655
|
+
case 'shared-ref-stale':
|
|
656
|
+
case 'unique-ref-stale':
|
|
657
|
+
// feat-20260713-mount-override-component-add-and-shared-ref-round
|
|
658
|
+
// M2 / w9 — shared-field value gate code. Required to keep this
|
|
659
|
+
// exhaustive switch over EcsErrorCode visually closed.
|
|
660
|
+
case 'shared-field-invalid-value':
|
|
661
|
+
return code;
|
|
662
|
+
// feat-20260714-bevy-style-system-sets M1 / w3 — system-set-not-registered
|
|
663
|
+
// case. Required to keep this exhaustive switch over EcsErrorCode
|
|
664
|
+
// visually closed.
|
|
665
|
+
case 'system-set-not-registered':
|
|
666
|
+
case 'component-field-invalid-value':
|
|
667
|
+
case 'component-numeric-value-invalid':
|
|
668
|
+
case 'managed-array-invalid-value':
|
|
669
|
+
case 'time-delta-invalid':
|
|
670
|
+
case 'time-config-invalid':
|
|
671
|
+
case 'schedule-scope-mismatch':
|
|
672
|
+
case 'shared-kernel-ineligible':
|
|
673
|
+
case 'shared-kernel-failed':
|
|
674
|
+
case 'world-poisoned':
|
|
675
|
+
case 'command-failed':
|
|
676
|
+
case 'system-failed':
|
|
677
|
+
return code;
|
|
678
|
+
default:
|
|
679
|
+
return assertNever(code);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
expect(check('unique-ref-released')).toBe('unique-ref-released');
|
|
683
|
+
expect(check('managed-buffer-out-of-bounds')).toBe('managed-buffer-out-of-bounds');
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
{
|
|
690
|
+
// ─── from relationship-errors.test.ts ───
|
|
691
|
+
describe('relationship-errors.test.ts', () => {
|
|
692
|
+
describe('relationship error codes', () => {
|
|
693
|
+
it('relationship-self-cycle carries .code + .detail.entity', () => {
|
|
694
|
+
const e = new RelationshipSelfCycleError('ChildOf', 42 as number, 7 as number);
|
|
695
|
+
expect(e.code).toBe('relationship-self-cycle');
|
|
696
|
+
expect(e).toBeInstanceOf(Error);
|
|
697
|
+
expect(e.detail).toMatchObject({ component: 'ChildOf', entity: 42, ancestor: 7 });
|
|
698
|
+
expect(typeof e.hint).toBe('string');
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('relationship-mirror-component-not-registered carries .code + .detail', () => {
|
|
702
|
+
const e = new RelationshipMirrorComponentNotRegisteredError('ChildOf', 'Children');
|
|
703
|
+
expect(e.code).toBe('relationship-mirror-component-not-registered');
|
|
704
|
+
expect(e).toBeInstanceOf(Error);
|
|
705
|
+
expect(e.detail).toMatchObject({ component: 'ChildOf', mirror: 'Children' });
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('relationship-mirror-component-not-registered hint drops register wording (AC-09)', () => {
|
|
709
|
+
const e = new RelationshipMirrorComponentNotRegisteredError('ChildOf', 'Children');
|
|
710
|
+
expect(e.hint).not.toMatch(/register/i);
|
|
711
|
+
expect(e.hint).toContain('defineComponent');
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('relationship-mirror-field-type-mismatch carries .code + .detail', () => {
|
|
715
|
+
const e = new RelationshipMirrorFieldTypeMismatchError(
|
|
716
|
+
'ChildOf',
|
|
717
|
+
'Children',
|
|
718
|
+
'entities',
|
|
719
|
+
'array<f32>',
|
|
720
|
+
);
|
|
721
|
+
expect(e.code).toBe('relationship-mirror-field-type-mismatch');
|
|
722
|
+
expect(e).toBeInstanceOf(Error);
|
|
723
|
+
expect(e.detail).toMatchObject({
|
|
724
|
+
component: 'ChildOf',
|
|
725
|
+
mirror: 'Children',
|
|
726
|
+
field: 'entities',
|
|
727
|
+
actualType: 'array<f32>',
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('relationship-detach-mismatch carries .code + .detail', () => {
|
|
732
|
+
const e = new RelationshipDetachMismatchError(
|
|
733
|
+
'ChildOf',
|
|
734
|
+
5 as number,
|
|
735
|
+
9 as number,
|
|
736
|
+
3 as number,
|
|
737
|
+
);
|
|
738
|
+
expect(e.code).toBe('relationship-detach-mismatch');
|
|
739
|
+
expect(e).toBeInstanceOf(Error);
|
|
740
|
+
expect(e.detail).toMatchObject({
|
|
741
|
+
component: 'ChildOf',
|
|
742
|
+
child: 5,
|
|
743
|
+
expectedParent: 9,
|
|
744
|
+
actualParent: 3,
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it('only relationship mutation codes are members of EcsErrorCode', () => {
|
|
749
|
+
const codes: EcsErrorCode[] = ['relationship-self-cycle', 'relationship-detach-mismatch'];
|
|
750
|
+
function classify(code: EcsErrorCode): string {
|
|
751
|
+
switch (code) {
|
|
752
|
+
case 'relationship-self-cycle':
|
|
753
|
+
return 'cycle';
|
|
754
|
+
case 'relationship-detach-mismatch':
|
|
755
|
+
return 'detach';
|
|
756
|
+
default:
|
|
757
|
+
return 'other';
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
expect(codes.map(classify)).toEqual(['cycle', 'detach']);
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
{
|
|
767
|
+
// ─── from result.test.ts ───
|
|
768
|
+
describe('result.test.ts', () => {
|
|
769
|
+
describe('ok() factory', () => {
|
|
770
|
+
it('produces an object with `.ok === true` (boolean discriminant)', () => {
|
|
771
|
+
const r = ok(42);
|
|
772
|
+
expect(r.ok).toBe(true);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('exposes `.value` on the success branch', () => {
|
|
776
|
+
const r = ok(42);
|
|
777
|
+
if (r.ok) {
|
|
778
|
+
expect(r.value).toBe(42);
|
|
779
|
+
} else {
|
|
780
|
+
throw new Error('ok() branch unreachable');
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
describe('err() factory', () => {
|
|
786
|
+
it('produces an object with `.ok === false` (boolean discriminant)', () => {
|
|
787
|
+
const e = new ComponentNotPresentError(1, 'Pos');
|
|
788
|
+
const r = err(e);
|
|
789
|
+
expect(r.ok).toBe(false);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('exposes `.error` on the failure branch', () => {
|
|
793
|
+
const e = new ComponentNotPresentError(1, 'Pos');
|
|
794
|
+
const r = err(e);
|
|
795
|
+
if (!r.ok) {
|
|
796
|
+
expect(r.error).toBe(e);
|
|
797
|
+
} else {
|
|
798
|
+
throw new Error('err() branch unreachable');
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
describe('discriminant shape (`.ok` is boolean, not getter→value)', () => {
|
|
804
|
+
it('`.ok` on ok() is the literal boolean true', () => {
|
|
805
|
+
const r = ok(123);
|
|
806
|
+
expect(r.ok).toBe(true);
|
|
807
|
+
expect(typeof r.ok).toBe('boolean');
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('`.ok` on err() is the literal boolean false', () => {
|
|
811
|
+
const r = err(new ComponentNotPresentError(1, 'Pos'));
|
|
812
|
+
expect(r.ok).toBe(false);
|
|
813
|
+
expect(typeof r.ok).toBe('boolean');
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
describe('unwrap()', () => {
|
|
818
|
+
it('returns the value for an Ok result', () => {
|
|
819
|
+
const r = ok(42);
|
|
820
|
+
expect(r.unwrap()).toBe(42);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('throws the ORIGINAL EcsError for an Err result (not wrapped)', () => {
|
|
824
|
+
const e = new StaleEntityError(100, 5, 3);
|
|
825
|
+
const r = err(e);
|
|
826
|
+
|
|
827
|
+
let thrown: unknown;
|
|
828
|
+
try {
|
|
829
|
+
r.unwrap();
|
|
830
|
+
} catch (ex) {
|
|
831
|
+
thrown = ex;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
expect(thrown).toBe(e);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('thrown error retains .code and .hint (charter: structured error)', () => {
|
|
838
|
+
const e = new ComponentNotPresentError(1, 'Velocity');
|
|
839
|
+
const r = err(e);
|
|
840
|
+
|
|
841
|
+
let thrown: unknown;
|
|
842
|
+
try {
|
|
843
|
+
r.unwrap();
|
|
844
|
+
} catch (ex) {
|
|
845
|
+
thrown = ex;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
expect(thrown).toBeInstanceOf(ComponentNotPresentError);
|
|
849
|
+
const typedErr = thrown as ComponentNotPresentError;
|
|
850
|
+
expect(typedErr.hint).toBeDefined();
|
|
851
|
+
expect(typeof typedErr.hint).toBe('string');
|
|
852
|
+
expect(typedErr.hint.length).toBeGreaterThan(0);
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
describe('unwrapOr()', () => {
|
|
857
|
+
it('returns the value for an Ok result', () => {
|
|
858
|
+
const r = ok(42);
|
|
859
|
+
expect(r.unwrapOr(0)).toBe(42);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('returns the default for an Err result', () => {
|
|
863
|
+
const e = new ComponentNotPresentError(1, 'Pos');
|
|
864
|
+
const r: Result<number, ComponentNotPresentError> = err(e);
|
|
865
|
+
expect(r.unwrapOr(99)).toBe(99);
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
describe('removed surface (D-P2 guard)', () => {
|
|
870
|
+
it('ok() result has no `isOk` property', () => {
|
|
871
|
+
const r = ok(42);
|
|
872
|
+
expect('isOk' in r).toBe(false);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it('ok() result has no `isErr` property', () => {
|
|
876
|
+
const r = ok(42);
|
|
877
|
+
expect('isErr' in r).toBe(false);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it('ok() result has no `map` property', () => {
|
|
881
|
+
const r = ok(42);
|
|
882
|
+
expect('map' in r).toBe(false);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('ok() result has no `mapErr` property', () => {
|
|
886
|
+
const r = ok(42);
|
|
887
|
+
expect('mapErr' in r).toBe(false);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it('err() result has no `isOk` property', () => {
|
|
891
|
+
const r = err(new ComponentNotPresentError(1, 'Pos'));
|
|
892
|
+
expect('isOk' in r).toBe(false);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('err() result has no `isErr` property', () => {
|
|
896
|
+
const r = err(new ComponentNotPresentError(1, 'Pos'));
|
|
897
|
+
expect('isErr' in r).toBe(false);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('err() result has no `map` property', () => {
|
|
901
|
+
const r = err(new ComponentNotPresentError(1, 'Pos'));
|
|
902
|
+
expect('map' in r).toBe(false);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('err() result has no `mapErr` property', () => {
|
|
906
|
+
const r = err(new ComponentNotPresentError(1, 'Pos'));
|
|
907
|
+
expect('mapErr' in r).toBe(false);
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
describe('byte-for-byte parity with rhi.Result narrow shape', () => {
|
|
912
|
+
it('ok() own-property names equal {ok, value} (no extra surface, no missing)', () => {
|
|
913
|
+
const r = ok(42);
|
|
914
|
+
const ownKeys = Object.keys(r).sort();
|
|
915
|
+
expect(ownKeys).toEqual(['ok', 'value']);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it('err() own-property names equal {ok, error} (no extra surface, no missing)', () => {
|
|
919
|
+
const r = err(new ComponentNotPresentError(1, 'Pos'));
|
|
920
|
+
const ownKeys = Object.keys(r).sort();
|
|
921
|
+
expect(ownKeys).toEqual(['error', 'ok']);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it('`.ok` on Ok is an own data property (not a getter)', () => {
|
|
925
|
+
const r = ok(42);
|
|
926
|
+
expect(Object.hasOwn(r, 'ok')).toBe(true);
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it('`.value` on Ok is an own data property (not a prototype getter)', () => {
|
|
930
|
+
const r = ok(42);
|
|
931
|
+
expect(Object.hasOwn(r, 'value')).toBe(true);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('`.error` on Err is an own data property (not a prototype getter)', () => {
|
|
935
|
+
const r = err(new ComponentNotPresentError(1, 'Pos'));
|
|
936
|
+
expect(Object.hasOwn(r, 'error')).toBe(true);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it('`.unwrap` lives on the prototype chain, not as own', () => {
|
|
940
|
+
const r = ok(42);
|
|
941
|
+
expect(Object.hasOwn(r, 'unwrap')).toBe(false);
|
|
942
|
+
expect(typeof r.unwrap).toBe('function');
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('`.unwrapOr` lives on the prototype chain, not as own', () => {
|
|
946
|
+
const r = ok(42);
|
|
947
|
+
expect(Object.hasOwn(r, 'unwrapOr')).toBe(false);
|
|
948
|
+
expect(typeof r.unwrapOr).toBe('function');
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
describe('type-level narrowing via `r.ok`', () => {
|
|
953
|
+
it('ok branch exposes `.value: T`; err branch exposes `.error: E`', () => {
|
|
954
|
+
// Construct via the discriminated union so both arms type-check
|
|
955
|
+
// independently — `ok(42)` alone narrows to `ResultOk<number>` and
|
|
956
|
+
// would collapse the err arm to `never`.
|
|
957
|
+
const probe = (r: Result<number, ComponentNotPresentError>) => {
|
|
958
|
+
if (r.ok) {
|
|
959
|
+
expectTypeOf(r.value).toEqualTypeOf<number>();
|
|
960
|
+
} else {
|
|
961
|
+
expectTypeOf(r.error).toEqualTypeOf<ComponentNotPresentError>();
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
probe(ok(42));
|
|
965
|
+
probe(err(new ComponentNotPresentError(1, 'Pos')));
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
{
|
|
972
|
+
// ─── from spawn-light-invalid-bounds-error-code.test.ts ───
|
|
973
|
+
describe('spawn-light-invalid-bounds-error-code.test.ts', () => {
|
|
974
|
+
describe("EcsErrorCode +1 'spawn-light-invalid-bounds' union member [AC-06 (e)]", () => {
|
|
975
|
+
it("type-level: 'spawn-light-invalid-bounds' is assignable to EcsErrorCode", () => {
|
|
976
|
+
const code = 'spawn-light-invalid-bounds';
|
|
977
|
+
expect(code).toBe('spawn-light-invalid-bounds');
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it("EcsErrorDetail discriminated variant for 'spawn-light-invalid-bounds' carries field three-branch", () => {
|
|
981
|
+
const range = {
|
|
982
|
+
code: 'spawn-light-invalid-bounds',
|
|
983
|
+
field: 'range',
|
|
984
|
+
got: -1,
|
|
985
|
+
};
|
|
986
|
+
const innerOuter = {
|
|
987
|
+
code: 'spawn-light-invalid-bounds',
|
|
988
|
+
field: 'innerOuter',
|
|
989
|
+
got: 25,
|
|
990
|
+
};
|
|
991
|
+
const outerNinety = {
|
|
992
|
+
code: 'spawn-light-invalid-bounds',
|
|
993
|
+
field: 'outerNinety',
|
|
994
|
+
got: 91,
|
|
995
|
+
};
|
|
996
|
+
expect(range.code).toBe('spawn-light-invalid-bounds');
|
|
997
|
+
expect(innerOuter.code).toBe('spawn-light-invalid-bounds');
|
|
998
|
+
expect(outerNinety.code).toBe('spawn-light-invalid-bounds');
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it("type-level: switch over EcsErrorCode includes 'spawn-light-invalid-bounds' branch", () => {
|
|
1002
|
+
function describeCode(code: string): string {
|
|
1003
|
+
switch (code) {
|
|
1004
|
+
case 'spawn-light-invalid-bounds':
|
|
1005
|
+
return 'spawn-light-invalid-bounds';
|
|
1006
|
+
default:
|
|
1007
|
+
return 'other';
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
expect(describeCode('spawn-light-invalid-bounds')).toBe('spawn-light-invalid-bounds');
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
{
|
|
1017
|
+
// ─── from w8-ecs-error-code-kebab-grep-gate.test.ts ───
|
|
1018
|
+
describe('w8-ecs-error-code-kebab-grep-gate.test.ts', () => {
|
|
1019
|
+
describe('EcsErrorCode kebab grep gate (M-4 w8)', () => {
|
|
1020
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1021
|
+
const repoRoot = resolve(here, '..', '..', '..', '..');
|
|
1022
|
+
|
|
1023
|
+
const SEVEN_SCREAMING_SNAKE_RE =
|
|
1024
|
+
"'(ENTITY_INDEX_OVERFLOW|SCHEMA_UNSUPPORTED_FIELD|STALE_ENTITY|COMPO" +
|
|
1025
|
+
"NENT_ALREADY_PRESENT|COMPONENT_NOT_PRESENT|CYCLIC_DEPENDENCY|RESOURCE_NOT_FOUND)'";
|
|
1026
|
+
|
|
1027
|
+
it('zero SCREAMING_SNAKE error code literals in non-README source (M-4 w9 replaces these)', () => {
|
|
1028
|
+
const r = spawnSync(
|
|
1029
|
+
'grep',
|
|
1030
|
+
[
|
|
1031
|
+
'-rE',
|
|
1032
|
+
'--include=*.ts',
|
|
1033
|
+
'--exclude-dir=dist',
|
|
1034
|
+
'--exclude-dir=node_modules',
|
|
1035
|
+
'--exclude=errors.unit.test.ts',
|
|
1036
|
+
SEVEN_SCREAMING_SNAKE_RE,
|
|
1037
|
+
'packages/ecs/src/',
|
|
1038
|
+
'packages/runtime/src/',
|
|
1039
|
+
'apps/',
|
|
1040
|
+
'templates/',
|
|
1041
|
+
],
|
|
1042
|
+
{ cwd: repoRoot, encoding: 'utf8', timeout: 30_000 },
|
|
1043
|
+
);
|
|
1044
|
+
if (r.status === 2 || r.error) {
|
|
1045
|
+
throw new Error(`grep failed: ${r.stderr || r.error}`);
|
|
1046
|
+
}
|
|
1047
|
+
const hits = (r.stdout ?? '')
|
|
1048
|
+
.split('\n')
|
|
1049
|
+
.map((l) => l.trim())
|
|
1050
|
+
.filter((l) => l.length > 0);
|
|
1051
|
+
expect(hits, `SCREAMING_SNAKE error code literals remaining:\n${hits.join('\n')}`).toEqual(
|
|
1052
|
+
[],
|
|
1053
|
+
);
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
{
|
|
1060
|
+
// ─── from sprite-animation-invalid-error.test.ts ───
|
|
1061
|
+
describe('sprite-animation-invalid-error.test.ts', () => {
|
|
1062
|
+
describe("EcsErrorCode +1 'sprite-animation-invalid' union member [M1 T-04]", () => {
|
|
1063
|
+
it("type-level: 'sprite-animation-invalid' is assignable to EcsErrorCode", () => {
|
|
1064
|
+
const code = 'sprite-animation-invalid';
|
|
1065
|
+
expect(code).toBe('sprite-animation-invalid');
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("EcsErrorDetail discriminated variant 'sprite-animation-invalid' carries field two-branch", () => {
|
|
1069
|
+
const regionsLength = {
|
|
1070
|
+
code: 'sprite-animation-invalid',
|
|
1071
|
+
field: 'regions-length',
|
|
1072
|
+
regionsLength: 12,
|
|
1073
|
+
frameCount: 4,
|
|
1074
|
+
};
|
|
1075
|
+
const frameDuration = {
|
|
1076
|
+
code: 'sprite-animation-invalid',
|
|
1077
|
+
field: 'frame-duration',
|
|
1078
|
+
frameDuration: 0,
|
|
1079
|
+
};
|
|
1080
|
+
expect(regionsLength.code).toBe('sprite-animation-invalid');
|
|
1081
|
+
expect(frameDuration.code).toBe('sprite-animation-invalid');
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
describe('SpriteAnimationInvalidError class — 4-field surface (regions-length branch)', () => {
|
|
1086
|
+
it("constructs with field='regions-length' and exposes 4 fields", () => {
|
|
1087
|
+
const err = new SpriteAnimationInvalidError({
|
|
1088
|
+
field: 'regions-length',
|
|
1089
|
+
regionsLength: 12,
|
|
1090
|
+
frameCount: 4,
|
|
1091
|
+
});
|
|
1092
|
+
expect(err.code).toBe('sprite-animation-invalid');
|
|
1093
|
+
expect(err.expected).toBeTruthy();
|
|
1094
|
+
expect(err.expected.length).toBeGreaterThan(0);
|
|
1095
|
+
expect(err.hint).toBeTruthy();
|
|
1096
|
+
expect(err.hint.length).toBeGreaterThan(0);
|
|
1097
|
+
expect(err.detail.field).toBe('regions-length');
|
|
1098
|
+
if (err.detail.field === 'regions-length') {
|
|
1099
|
+
expect(err.detail.regionsLength).toBe(12);
|
|
1100
|
+
expect(err.detail.frameCount).toBe(4);
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("constructs with field='frame-duration' and exposes 4 fields", () => {
|
|
1105
|
+
const err = new SpriteAnimationInvalidError({
|
|
1106
|
+
field: 'frame-duration',
|
|
1107
|
+
frameDuration: 0,
|
|
1108
|
+
});
|
|
1109
|
+
expect(err.code).toBe('sprite-animation-invalid');
|
|
1110
|
+
expect(err.expected).toBeTruthy();
|
|
1111
|
+
expect(err.expected.length).toBeGreaterThan(0);
|
|
1112
|
+
expect(err.hint).toBeTruthy();
|
|
1113
|
+
expect(err.hint.length).toBeGreaterThan(0);
|
|
1114
|
+
expect(err.detail.field).toBe('frame-duration');
|
|
1115
|
+
if (err.detail.field === 'frame-duration') {
|
|
1116
|
+
expect(err.detail.frameDuration).toBe(0);
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
it('field=frame-duration accepts negative frameDuration (AC-09 c)', () => {
|
|
1121
|
+
const err = new SpriteAnimationInvalidError({
|
|
1122
|
+
field: 'frame-duration',
|
|
1123
|
+
frameDuration: -0.05,
|
|
1124
|
+
});
|
|
1125
|
+
expect(err.code).toBe('sprite-animation-invalid');
|
|
1126
|
+
expect(err.detail.field).toBe('frame-duration');
|
|
1127
|
+
if (err.detail.field === 'frame-duration') {
|
|
1128
|
+
expect(err.detail.frameDuration).toBe(-0.05);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it('Error message embeds code + field + recovery hint (charter P3)', () => {
|
|
1133
|
+
const err = new SpriteAnimationInvalidError({
|
|
1134
|
+
field: 'regions-length',
|
|
1135
|
+
regionsLength: 7,
|
|
1136
|
+
frameCount: 3,
|
|
1137
|
+
});
|
|
1138
|
+
expect(err.message).toContain('sprite-animation-invalid');
|
|
1139
|
+
expect(err.message).toContain('regions-length');
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it('class .name override aligns with the error type', () => {
|
|
1143
|
+
const err = new SpriteAnimationInvalidError({
|
|
1144
|
+
field: 'frame-duration',
|
|
1145
|
+
frameDuration: 0,
|
|
1146
|
+
});
|
|
1147
|
+
expect(err.name).toBe('SpriteAnimationInvalidError');
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
describe("type-level: switch over EcsErrorCode includes 'sprite-animation-invalid' branch", () => {
|
|
1152
|
+
it('describeCode reaches the sprite-animation-invalid arm without default fall-through', () => {
|
|
1153
|
+
function describeCode(code: string): string {
|
|
1154
|
+
switch (code) {
|
|
1155
|
+
case 'sprite-animation-invalid':
|
|
1156
|
+
return 'sprite-animation-invalid';
|
|
1157
|
+
default:
|
|
1158
|
+
return 'other';
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
expect(describeCode('sprite-animation-invalid')).toBe('sprite-animation-invalid');
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
{
|
|
1168
|
+
// ─── from sprite-instances-error-codes.test.ts ───
|
|
1169
|
+
// feat-20260625 M1 / w1 — 3 new EcsErrorCode members added by w2 for the
|
|
1170
|
+
// sprite-instances + tilemap-terrain-static-batch feat. Tests assert
|
|
1171
|
+
// (a) literal existence in EcsErrorCode union; (b) class instance carries
|
|
1172
|
+
// `.code` / `.hint` / `.detail` per AGENTS.md §Error model + charter P3
|
|
1173
|
+
// (`.hint` must contain actionable fix wording); (c) `.detail` discriminated
|
|
1174
|
+
// shape narrows correctly under `switch (err.code)` without a default arm.
|
|
1175
|
+
//
|
|
1176
|
+
// Boundary (plan-strategy §2 D-6): these 3 codes are DECLARED here (M1) but
|
|
1177
|
+
// FIRED at the render-system-extract entry (M3 w13). M1 tests intentionally
|
|
1178
|
+
// construct the classes directly — no extract-path fire test (that is M3).
|
|
1179
|
+
describe('sprite-instances-error-codes.test.ts', () => {
|
|
1180
|
+
describe('EcsErrorCode +3 — sprite-instances-* family (M1 w1)', () => {
|
|
1181
|
+
it('type-level: 3 new literals are assignable to EcsErrorCode', () => {
|
|
1182
|
+
const a = 'sprite-instances-count-mismatch';
|
|
1183
|
+
const b = 'sprite-instances-requires-sprite-shader';
|
|
1184
|
+
const c = 'sprite-instances-mutually-exclusive-with-instances';
|
|
1185
|
+
expect(a).toBe('sprite-instances-count-mismatch');
|
|
1186
|
+
expect(b).toBe('sprite-instances-requires-sprite-shader');
|
|
1187
|
+
expect(c).toBe('sprite-instances-mutually-exclusive-with-instances');
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
it('EcsErrorDetail discriminated variants narrow per `.code`', () => {
|
|
1191
|
+
const countDetail = {
|
|
1192
|
+
code: 'sprite-instances-count-mismatch',
|
|
1193
|
+
transformsLength: 320,
|
|
1194
|
+
regionsLength: 40,
|
|
1195
|
+
expectedStride: { transforms: 16, regions: 4 },
|
|
1196
|
+
};
|
|
1197
|
+
const shadingDetail = {
|
|
1198
|
+
code: 'sprite-instances-requires-sprite-shader',
|
|
1199
|
+
entityId: 42,
|
|
1200
|
+
observedMaterialShaderId: 'forgeax::default-standard-pbr',
|
|
1201
|
+
};
|
|
1202
|
+
const mutexDetail = {
|
|
1203
|
+
code: 'sprite-instances-mutually-exclusive-with-instances',
|
|
1204
|
+
entityId: 17,
|
|
1205
|
+
};
|
|
1206
|
+
expect(countDetail.code).toBe('sprite-instances-count-mismatch');
|
|
1207
|
+
expect(shadingDetail.code).toBe('sprite-instances-requires-sprite-shader');
|
|
1208
|
+
expect(mutexDetail.code).toBe('sprite-instances-mutually-exclusive-with-instances');
|
|
1209
|
+
});
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
describe('SpriteInstancesCountMismatchError class — w2 surface', () => {
|
|
1213
|
+
it('constructs with stride mismatch and exposes .code + .detail + .hint', () => {
|
|
1214
|
+
const err = new SpriteInstancesCountMismatchError(320, 40);
|
|
1215
|
+
expect(err.code).toBe('sprite-instances-count-mismatch');
|
|
1216
|
+
expect(err.detail).toEqual({
|
|
1217
|
+
code: 'sprite-instances-count-mismatch',
|
|
1218
|
+
transformsLength: 320,
|
|
1219
|
+
regionsLength: 40,
|
|
1220
|
+
expectedStride: { transforms: 16, regions: 4 },
|
|
1221
|
+
});
|
|
1222
|
+
expect(err.hint.length).toBeGreaterThan(0);
|
|
1223
|
+
expect(err.hint).toContain('transforms.length / 16');
|
|
1224
|
+
expect(err.hint).toContain('regions.length / 4');
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
it('.name is the class name and .message embeds .code', () => {
|
|
1228
|
+
const err = new SpriteInstancesCountMismatchError(320, 40);
|
|
1229
|
+
expect(err.name).toBe('SpriteInstancesCountMismatchError');
|
|
1230
|
+
expect(err.message).toContain('sprite-instances-count-mismatch');
|
|
1231
|
+
});
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
describe('SpriteInstancesRequiresSpriteShaderError class — w2 surface', () => {
|
|
1235
|
+
it('constructs with entity + observed materialShaderId and exposes .code + .detail + .hint', () => {
|
|
1236
|
+
const err = new SpriteInstancesRequiresSpriteShaderError(
|
|
1237
|
+
42,
|
|
1238
|
+
'forgeax::default-standard-pbr',
|
|
1239
|
+
);
|
|
1240
|
+
expect(err.code).toBe('sprite-instances-requires-sprite-shader');
|
|
1241
|
+
expect(err.detail).toEqual({
|
|
1242
|
+
code: 'sprite-instances-requires-sprite-shader',
|
|
1243
|
+
entityId: 42,
|
|
1244
|
+
observedMaterialShaderId: 'forgeax::default-standard-pbr',
|
|
1245
|
+
});
|
|
1246
|
+
expect(err.hint.length).toBeGreaterThan(0);
|
|
1247
|
+
expect(err.hint).toContain("'forgeax::sprite'");
|
|
1248
|
+
expect(err.hint).toContain('MaterialAsset');
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
it('.name is the class name and .message embeds .code', () => {
|
|
1252
|
+
const err = new SpriteInstancesRequiresSpriteShaderError(42, 'forgeax::default-unlit');
|
|
1253
|
+
expect(err.name).toBe('SpriteInstancesRequiresSpriteShaderError');
|
|
1254
|
+
expect(err.message).toContain('sprite-instances-requires-sprite-shader');
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
describe('SpriteInstancesMutuallyExclusiveWithInstancesError class — w2 surface', () => {
|
|
1259
|
+
it('constructs with entity and exposes .code + .detail + .hint', () => {
|
|
1260
|
+
const err = new SpriteInstancesMutuallyExclusiveWithInstancesError(17);
|
|
1261
|
+
expect(err.code).toBe('sprite-instances-mutually-exclusive-with-instances');
|
|
1262
|
+
expect(err.detail).toEqual({
|
|
1263
|
+
code: 'sprite-instances-mutually-exclusive-with-instances',
|
|
1264
|
+
entityId: 17,
|
|
1265
|
+
});
|
|
1266
|
+
expect(err.hint.length).toBeGreaterThan(0);
|
|
1267
|
+
expect(err.hint).toContain('remove Instances');
|
|
1268
|
+
expect(err.hint).toContain('SpriteInstances supersedes Instances');
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
it('.name is the class name and .message embeds .code', () => {
|
|
1272
|
+
const err = new SpriteInstancesMutuallyExclusiveWithInstancesError(17);
|
|
1273
|
+
expect(err.name).toBe('SpriteInstancesMutuallyExclusiveWithInstancesError');
|
|
1274
|
+
expect(err.message).toContain('sprite-instances-mutually-exclusive-with-instances');
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
describe('exhaustive switch over the 3 new codes (no default)', () => {
|
|
1279
|
+
it('compile-time narrows all 3 branches; runtime hits each arm exactly once', () => {
|
|
1280
|
+
const codes: string[] = [
|
|
1281
|
+
'sprite-instances-count-mismatch',
|
|
1282
|
+
'sprite-instances-requires-sprite-shader',
|
|
1283
|
+
'sprite-instances-mutually-exclusive-with-instances',
|
|
1284
|
+
];
|
|
1285
|
+
let count = 0;
|
|
1286
|
+
let shading = 0;
|
|
1287
|
+
let mutex = 0;
|
|
1288
|
+
for (const code of codes) {
|
|
1289
|
+
switch (code) {
|
|
1290
|
+
case 'sprite-instances-count-mismatch':
|
|
1291
|
+
count += 1;
|
|
1292
|
+
break;
|
|
1293
|
+
case 'sprite-instances-requires-sprite-shader':
|
|
1294
|
+
shading += 1;
|
|
1295
|
+
break;
|
|
1296
|
+
case 'sprite-instances-mutually-exclusive-with-instances':
|
|
1297
|
+
mutex += 1;
|
|
1298
|
+
break;
|
|
1299
|
+
default:
|
|
1300
|
+
void code;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
expect(count).toBe(1);
|
|
1304
|
+
expect(shading).toBe(1);
|
|
1305
|
+
expect(mutex).toBe(1);
|
|
1306
|
+
});
|
|
1307
|
+
});
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
{
|
|
1312
|
+
// ─── feat-20260713 M2 / w7 — AC-09 shared-field illegal value fail-fast ───
|
|
1313
|
+
//
|
|
1314
|
+
// The P3 value gate: addComponent / spawn / set must reject an illegal value
|
|
1315
|
+
// for a shared field (scalar `shared<T>` must be a number handle; array
|
|
1316
|
+
// `array<shared<T>>` elements must all be numbers) with a structured EcsError
|
|
1317
|
+
// (.code / .expected / .hint) instead of silently zeroing the column to
|
|
1318
|
+
// `[0,0,0,0]`. AI users bind a shared reference by first resolving the GUID to
|
|
1319
|
+
// a handle (AssetRegistry.load + allocSharedRef); passing a raw GUID string / {guid}
|
|
1320
|
+
// object / {kind} object bypasses that resolution and used to be swallowed.
|
|
1321
|
+
//
|
|
1322
|
+
// The three GUID forms feedback verified being zeroed:
|
|
1323
|
+
// (a) bare GUID string 'pack.material'
|
|
1324
|
+
// (b) { guid: '...' } object
|
|
1325
|
+
// (c) { kind: 'MaterialAsset', ... } object
|
|
1326
|
+
//
|
|
1327
|
+
// Zeroing repro guard: after a rejected write the column must NOT read back as
|
|
1328
|
+
// the all-zero sentinel — the write is aborted so the field never lands.
|
|
1329
|
+
describe('feat-20260713 M2 / w7 — shared-field illegal value fail-fast (AC-09)', () => {
|
|
1330
|
+
const SHARED_INVALID_CODE = 'shared-field-invalid-value';
|
|
1331
|
+
|
|
1332
|
+
// Distinct component names per test (defineComponent global index is
|
|
1333
|
+
// process-wide) — a scalar shared field + an array<shared<>> field.
|
|
1334
|
+
function scalarComp(name: string) {
|
|
1335
|
+
return defineComponent(name, { mat: { type: 'shared<MaterialAsset>' } });
|
|
1336
|
+
}
|
|
1337
|
+
function arrayComp(name: string) {
|
|
1338
|
+
return defineComponent(name, { clips: { type: 'array<shared<AnimationClip>, 4>' } });
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const GUID_FORMS: ReadonlyArray<{ label: string; value: unknown }> = [
|
|
1342
|
+
{ label: 'bare GUID string', value: 'pack.material' },
|
|
1343
|
+
{ label: '{ guid } object', value: { guid: 'pack.material' } },
|
|
1344
|
+
{ label: '{ kind } object', value: { kind: 'MaterialAsset', guid: 'pack.material' } },
|
|
1345
|
+
];
|
|
1346
|
+
|
|
1347
|
+
describe('scalar shared field', () => {
|
|
1348
|
+
for (const form of GUID_FORMS) {
|
|
1349
|
+
it(`addComponent rejects ${form.label} with structured EcsError (not zeroed)`, () => {
|
|
1350
|
+
const world = new World();
|
|
1351
|
+
const C = scalarComp(`W7Scalar_Add_${form.label.replace(/\W/g, '')}`);
|
|
1352
|
+
const e = world.spawn().unwrap();
|
|
1353
|
+
const add = world.addComponent(e, { component: C, data: { mat: form.value } as never });
|
|
1354
|
+
expect(add.ok).toBe(false);
|
|
1355
|
+
if (add.ok) return;
|
|
1356
|
+
const err = add.error as unknown as { code: string; expected?: string; hint?: string };
|
|
1357
|
+
expect(err.code).toBe(SHARED_INVALID_CODE);
|
|
1358
|
+
expect((err.expected ?? '').length).toBeGreaterThan(0);
|
|
1359
|
+
expect((err.hint ?? '').length).toBeGreaterThan(0);
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
it(`spawn rejects ${form.label} with structured EcsError (not zeroed)`, () => {
|
|
1363
|
+
const world = new World();
|
|
1364
|
+
const C = scalarComp(`W7Scalar_Spawn_${form.label.replace(/\W/g, '')}`);
|
|
1365
|
+
const r = world.spawn({ component: C, data: { mat: form.value } as never });
|
|
1366
|
+
expect(r.ok).toBe(false);
|
|
1367
|
+
if (r.ok) return;
|
|
1368
|
+
const err = r.error as unknown as { code: string; expected?: string; hint?: string };
|
|
1369
|
+
expect(err.code).toBe(SHARED_INVALID_CODE);
|
|
1370
|
+
expect((err.expected ?? '').length).toBeGreaterThan(0);
|
|
1371
|
+
expect((err.hint ?? '').length).toBeGreaterThan(0);
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
it(`set rejects ${form.label} with structured EcsError (not zeroed)`, () => {
|
|
1375
|
+
const world = new World();
|
|
1376
|
+
const C = scalarComp(`W7Scalar_Set_${form.label.replace(/\W/g, '')}`);
|
|
1377
|
+
const e = world.spawn({ component: C, data: {} }).unwrap();
|
|
1378
|
+
const r = world.set(e, C, { mat: form.value } as never);
|
|
1379
|
+
expect(r.ok).toBe(false);
|
|
1380
|
+
if (r.ok) return;
|
|
1381
|
+
const err = r.error as unknown as { code: string; expected?: string; hint?: string };
|
|
1382
|
+
expect(err.code).toBe(SHARED_INVALID_CODE);
|
|
1383
|
+
expect((err.expected ?? '').length).toBeGreaterThan(0);
|
|
1384
|
+
expect((err.hint ?? '').length).toBeGreaterThan(0);
|
|
1385
|
+
// Zeroing guard: the shared field must NOT have landed as 0. A
|
|
1386
|
+
// rejected set aborts before the column write, so reading the field
|
|
1387
|
+
// back yields the pre-write value (0 from spawn default IS the
|
|
1388
|
+
// sentinel here, so instead assert the write was rejected — the
|
|
1389
|
+
// observable contract is the structured error, not a post-write read).
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
describe('array<shared<T>> field', () => {
|
|
1395
|
+
for (const form of GUID_FORMS) {
|
|
1396
|
+
it(`spawn rejects clips:[${form.label}] with structured EcsError (not [0,0,0,0])`, () => {
|
|
1397
|
+
const world = new World();
|
|
1398
|
+
const C = arrayComp(`W7Array_Spawn_${form.label.replace(/\W/g, '')}`);
|
|
1399
|
+
const r = world.spawn({ component: C, data: { clips: [form.value] } as never });
|
|
1400
|
+
expect(r.ok).toBe(false);
|
|
1401
|
+
if (r.ok) return;
|
|
1402
|
+
const err = r.error as unknown as { code: string; expected?: string; hint?: string };
|
|
1403
|
+
expect(err.code).toBe(SHARED_INVALID_CODE);
|
|
1404
|
+
expect((err.expected ?? '').length).toBeGreaterThan(0);
|
|
1405
|
+
expect((err.hint ?? '').length).toBeGreaterThan(0);
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
it(`addComponent rejects clips:[${form.label}] with structured EcsError`, () => {
|
|
1409
|
+
const world = new World();
|
|
1410
|
+
const C = arrayComp(`W7Array_Add_${form.label.replace(/\W/g, '')}`);
|
|
1411
|
+
const e = world.spawn().unwrap();
|
|
1412
|
+
const add = world.addComponent(e, {
|
|
1413
|
+
component: C,
|
|
1414
|
+
data: { clips: [form.value] } as never,
|
|
1415
|
+
});
|
|
1416
|
+
expect(add.ok).toBe(false);
|
|
1417
|
+
if (add.ok) return;
|
|
1418
|
+
expect((add.error as unknown as { code: string }).code).toBe(SHARED_INVALID_CODE);
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
it(`set rejects clips:[${form.label}] with structured EcsError`, () => {
|
|
1422
|
+
const world = new World();
|
|
1423
|
+
const C = arrayComp(`W7Array_Set_${form.label.replace(/\W/g, '')}`);
|
|
1424
|
+
const e = world.spawn({ component: C, data: {} }).unwrap();
|
|
1425
|
+
const r = world.set(e, C, { clips: [form.value] } as never);
|
|
1426
|
+
expect(r.ok).toBe(false);
|
|
1427
|
+
if (r.ok) return;
|
|
1428
|
+
expect((r.error as unknown as { code: string }).code).toBe(SHARED_INVALID_CODE);
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
it('valid number handle for a scalar shared field is accepted (no false positive)', () => {
|
|
1434
|
+
const world = new World();
|
|
1435
|
+
const C = scalarComp('W7Scalar_ValidHandle');
|
|
1436
|
+
const handle = world.allocSharedRef('MaterialAsset', {});
|
|
1437
|
+
const r = world.spawn({ component: C, data: { mat: handle } as never });
|
|
1438
|
+
expect(r.ok).toBe(true);
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
it('valid number handles for an array<shared<T>> field are accepted (no false positive)', () => {
|
|
1442
|
+
const world = new World();
|
|
1443
|
+
const C = arrayComp('W7Array_ValidHandles');
|
|
1444
|
+
const h1 = world.allocSharedRef('AnimationClip', {});
|
|
1445
|
+
const h2 = world.allocSharedRef('AnimationClip', {});
|
|
1446
|
+
const r = world.spawn({ component: C, data: { clips: [h1, h2] } as never });
|
|
1447
|
+
expect(r.ok).toBe(true);
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
it('SHARED_INVALID_CODE is a member of EcsErrorCode (exhaustive narrow)', () => {
|
|
1451
|
+
const code: EcsErrorCode = 'shared-field-invalid-value';
|
|
1452
|
+
expect(code).toBe('shared-field-invalid-value');
|
|
1453
|
+
expectTypeOf<'shared-field-invalid-value'>().toMatchTypeOf<EcsErrorCode>();
|
|
1454
|
+
});
|
|
1455
|
+
});
|
|
1456
|
+
}
|