@pixel-point/toolcraft 0.0.17 → 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.
Files changed (196) hide show
  1. package/package.json +1 -1
  2. package/src/generate-test-doc-assertions.mjs +58 -68
  3. package/src/generate-test-doc-assertions.test.mjs +43 -0
  4. package/src/generate-test-doc-policy-assertions.mjs +92 -0
  5. package/src/generate-test-file-assertions.mjs +26 -75
  6. package/src/generate-test-product-iteration.test.mjs +17 -0
  7. package/src/generate-test-runtime-boundary-assertions.mjs +158 -0
  8. package/src/generate-test-standalone-assertions.mjs +10 -0
  9. package/templates/runtime/contracts/component-contracts.choices.test.ts +47 -3
  10. package/templates/runtime/contracts/component-contracts.choices.ts +48 -3
  11. package/templates/runtime/contracts/component-contracts.inputs.test.ts +6 -6
  12. package/templates/runtime/contracts/component-contracts.inputs.ts +6 -6
  13. package/templates/runtime/contracts/component-contracts.media-custom.test.ts +8 -2
  14. package/templates/runtime/contracts/component-contracts.media-custom.ts +8 -2
  15. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +4 -4
  16. package/templates/runtime/contracts/component-contracts.runtime-labels.test.ts +7 -1
  17. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +3 -0
  18. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +3 -0
  19. package/templates/runtime/contracts/component-contracts.runtime.ts +12 -8
  20. package/templates/runtime/contracts/component-contracts.test.ts +2 -0
  21. package/templates/runtime/contracts/decision-contracts.test.ts +18 -0
  22. package/templates/runtime/contracts/decision-contracts.ts +10 -10
  23. package/templates/runtime/export/export-background.ts +0 -19
  24. package/templates/runtime/export/export.test.ts +45 -42
  25. package/templates/runtime/export/index.ts +44 -16
  26. package/templates/runtime/index.ts +2 -0
  27. package/templates/runtime/model-import/model-import-limits.ts +15 -4
  28. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +11 -1
  29. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +29 -0
  30. package/templates/runtime/react/app-shell/toolcraft-app.tsx +18 -8
  31. package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +76 -0
  32. package/templates/runtime/react/canvas/canvas-shell-test-utils.tsx +13 -1
  33. package/templates/runtime/react/canvas/canvas-shell.tsx +16 -1
  34. package/templates/runtime/react/canvas/product-scene-surface.test.tsx +195 -0
  35. package/templates/runtime/react/canvas/product-scene-surface.tsx +155 -0
  36. package/templates/runtime/react/canvas/runtime-scene-export.ts +1 -1
  37. package/templates/runtime/react/controls-panel/__tests__/control-conditions.test.ts +61 -0
  38. package/templates/runtime/react/controls-panel/__tests__/controls-panel.collection-actions.test.tsx +89 -0
  39. package/templates/runtime/react/controls-panel/__tests__/controls-panel.control-visibility.test.tsx +40 -19
  40. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +1 -1
  41. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-collection.test.tsx +62 -0
  42. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-color-bank.test.tsx +67 -0
  43. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +2 -4
  44. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +10 -4
  45. package/templates/runtime/react/controls-panel/conditions/control-conditions.ts +32 -119
  46. package/templates/runtime/react/controls-panel/layout/controls-panel-control-group.tsx +5 -2
  47. package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +10 -8
  48. package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +3 -3
  49. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.test.tsx +63 -0
  50. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.tsx +138 -0
  51. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-items.tsx +104 -0
  52. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-renderer.tsx +115 -236
  53. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.test.tsx +95 -0
  54. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.tsx +82 -0
  55. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.test.tsx +73 -1
  56. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.tsx +50 -1
  57. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.test.ts +1 -1
  58. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.ts +2 -1
  59. package/templates/runtime/react/controls-panel/values/controls-panel-collection-values.ts +38 -6
  60. package/templates/runtime/react/controls-panel/values/controls-panel-value-labels.ts +1 -0
  61. package/templates/runtime/react/index.ts +4 -0
  62. package/templates/runtime/react/model-rendering/model-export.ts +1 -1
  63. package/templates/runtime/react/orientation-gizmo/orientation-gizmo-selection.ts +5 -5
  64. package/templates/runtime/react/orientation-gizmo/use-toolcraft-orientation-control-selection.ts +2 -2
  65. package/templates/runtime/scene/scene-bounds.test.ts +23 -0
  66. package/templates/runtime/scene/scene-bounds.ts +31 -17
  67. package/templates/runtime/schema/app-capabilities.test.ts +4 -0
  68. package/templates/runtime/schema/collection-item-controls.ts +29 -0
  69. package/templates/runtime/schema/control-applicability.test.ts +127 -0
  70. package/templates/runtime/schema/control-applicability.ts +433 -0
  71. package/templates/runtime/schema/control-schema-normalization.ts +177 -11
  72. package/templates/runtime/schema/controls-panel-actions.ts +2 -2
  73. package/templates/runtime/schema/controls-panel-section-layout.ts +14 -9
  74. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +59 -0
  75. package/templates/runtime/schema/define-toolcraft.collection-actions-item-controls.test.ts +127 -0
  76. package/templates/runtime/schema/define-toolcraft.control-sections.test.ts +10 -4
  77. package/templates/runtime/schema/define-toolcraft.file-drop-item-controls.test.ts +98 -0
  78. package/templates/runtime/schema/define-toolcraft.file-drop-normalization.test.ts +27 -0
  79. package/templates/runtime/schema/define-toolcraft.segmented-control-fit.test.ts +20 -2
  80. package/templates/runtime/schema/define-toolcraft.setup-canvas.test.ts +16 -3
  81. package/templates/runtime/schema/define-toolcraft.source-collection.test.ts +79 -0
  82. package/templates/runtime/schema/runtime-setup-section.ts +24 -5
  83. package/templates/runtime/schema/types.ts +68 -13
  84. package/templates/starter/AGENTS.md +12 -10
  85. package/templates/starter/docs/toolcraft/acceptance-testing.md +17 -31
  86. package/templates/starter/docs/toolcraft/agent-worklog.md +15 -0
  87. package/templates/starter/docs/toolcraft/assembly-workflow.md +10 -3
  88. package/templates/starter/docs/toolcraft/component-rules.md +8 -6
  89. package/templates/starter/docs/toolcraft/core/control-selection.md +9 -3
  90. package/templates/starter/docs/toolcraft/core/layout.md +7 -7
  91. package/templates/starter/docs/toolcraft/core/media-upload.md +2 -0
  92. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +13 -2
  93. package/templates/starter/docs/toolcraft/core/setup-export.md +33 -7
  94. package/templates/starter/docs/toolcraft/custom-controls.md +6 -4
  95. package/templates/starter/docs/toolcraft/decision-contract.md +3 -1
  96. package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
  97. package/templates/starter/docs/toolcraft/schema-reference.md +21 -19
  98. package/templates/starter/e2e/app-browser-control-applicability-requirements.spec.ts +163 -0
  99. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +7 -2
  100. package/templates/starter/e2e/app-browser-orientation-evidence.spec.ts +174 -21
  101. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +1 -33
  102. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +61 -15
  103. package/templates/starter/e2e/{browser-conditional-output-evidence-helpers.ts → browser-background-output-evidence.ts} +0 -68
  104. package/templates/starter/e2e/browser-control-applicability-evidence.ts +114 -0
  105. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +20 -11
  106. package/templates/starter/e2e/browser-orientation-gizmo-evidence-helpers.ts +4 -0
  107. package/templates/starter/e2e/browser-orientation-gizmo-live-preconditions.ts +86 -0
  108. package/templates/starter/e2e/browser-runtime-applicability-requirements.ts +84 -0
  109. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +9 -2
  110. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +29 -26
  111. package/templates/starter/e2e/browser-semantic-evidence-test-helpers.ts +25 -0
  112. package/templates/starter/scripts/toolcraft-contract-manifest.json +1 -0
  113. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +85 -115
  114. package/templates/starter/scripts/toolcraft-product-boundary-module-policy.mjs +101 -0
  115. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +0 -72
  116. package/templates/starter/scripts/toolcraft-product-control-bindings.mjs +182 -0
  117. package/templates/starter/scripts/toolcraft-product-control-boundary.mjs +146 -0
  118. package/templates/starter/scripts/toolcraft-product-control-boundary.test.mjs +186 -0
  119. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +6 -17
  120. package/templates/starter/scripts/toolcraft-product-export-boundary.test.mjs +196 -0
  121. package/templates/starter/scripts/toolcraft-typescript-source-evidence-structure.test.mjs +20 -0
  122. package/templates/starter/scripts/toolcraft-typescript-source-evidence.mjs +1 -0
  123. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +2 -0
  124. package/templates/starter/src/app/acceptance/artifact-export-intent.ts +130 -0
  125. package/templates/starter/src/app/acceptance/conditions.ts +1 -1
  126. package/templates/starter/src/app/acceptance/control-acceptance-context.ts +2 -2
  127. package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +6 -20
  128. package/templates/starter/src/app/acceptance/control-acceptance-kind-rules.ts +24 -5
  129. package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +2 -13
  130. package/templates/starter/src/app/acceptance/control-applicability-cases.test.ts +321 -0
  131. package/templates/starter/src/app/acceptance/control-applicability-cases.ts +290 -0
  132. package/templates/starter/src/app/acceptance/control-applicability.ts +332 -0
  133. package/templates/starter/src/app/acceptance/control-layout-dependency-rules.ts +20 -9
  134. package/templates/starter/src/app/acceptance/control-layout-entity-rules.ts +4 -6
  135. package/templates/starter/src/app/acceptance/control-layout-model.ts +4 -4
  136. package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +3 -10
  137. package/templates/starter/src/app/acceptance/control-parts.ts +8 -0
  138. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.test.ts +215 -0
  139. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.ts +132 -0
  140. package/templates/starter/src/app/acceptance/control-section-inventory.test.ts +87 -0
  141. package/templates/starter/src/app/acceptance/control-section-inventory.ts +74 -44
  142. package/templates/starter/src/app/acceptance/custom-controls.ts +11 -3
  143. package/templates/starter/src/app/acceptance/inline-layout.ts +64 -9
  144. package/templates/starter/src/app/acceptance/media-upload.ts +5 -1
  145. package/templates/starter/src/app/acceptance/orientation-gizmo.ts +18 -11
  146. package/templates/starter/src/app/acceptance/output-background-rules.ts +2 -2
  147. package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +18 -10
  148. package/templates/starter/src/app/acceptance/output-export-model.ts +54 -11
  149. package/templates/starter/src/app/acceptance/output-export.ts +22 -10
  150. package/templates/starter/src/app/acceptance/output-video-export-rules.ts +99 -0
  151. package/templates/starter/src/app/acceptance/types.ts +23 -8
  152. package/templates/starter/src/app/acceptance/validate-coverage.ts +19 -2
  153. package/templates/starter/src/app/starter-acceptance.artifact-export-intent.test.ts +239 -0
  154. package/templates/starter/src/app/starter-acceptance.background-export.test.ts +33 -0
  155. package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +16 -1
  156. package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +65 -0
  157. package/templates/starter/src/app/starter-acceptance.contract-fixtures.test.ts +41 -0
  158. package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +112 -16
  159. package/templates/starter/src/app/starter-acceptance.control-applicability.test.ts +224 -0
  160. package/templates/starter/src/app/starter-acceptance.control-order.test.ts +58 -3
  161. package/templates/starter/src/app/starter-acceptance.control-state.test.ts +0 -111
  162. package/templates/starter/src/app/starter-acceptance.custom-control-collections.test.ts +3 -1
  163. package/templates/starter/src/app/starter-acceptance.custom-control-valid.test.ts +1 -1
  164. package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +22 -0
  165. package/templates/starter/src/app/{starter-acceptance.image-video-export-settings.test.ts → starter-acceptance.image-export-settings.test.ts} +37 -9
  166. package/templates/starter/src/app/starter-acceptance.interaction-ownership.test.ts +12 -0
  167. package/templates/starter/src/app/starter-acceptance.media-upload.fixtures.ts +11 -1
  168. package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +28 -0
  169. package/templates/starter/src/app/starter-acceptance.output-export-intent.test.ts +384 -0
  170. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +4 -0
  171. package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +112 -105
  172. package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +12 -0
  173. package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +1 -1
  174. package/templates/starter/src/app/starter-acceptance.section-dependencies.test.ts +35 -24
  175. package/templates/starter/src/app/starter-acceptance.section-size.test.ts +72 -0
  176. package/templates/starter/src/app/starter-acceptance.ts +11 -2
  177. package/templates/starter/src/app/starter-acceptance.video-export-settings.test.ts +336 -0
  178. package/templates/starter/src/app/starter-acceptance.view-interaction.test.ts +8 -0
  179. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -2
  180. package/templates/starter/src/routes/index.tsx +1 -0
  181. package/templates/ui/components/composites/tabs.tsx +2 -2
  182. package/templates/ui/components/controls/collection-actions/collection-item-groups.test.tsx +38 -0
  183. package/templates/ui/components/controls/collection-actions/collection-item-groups.tsx +36 -0
  184. package/templates/ui/components/controls/collection-actions/index.ts +2 -0
  185. package/templates/ui/components/controls/file-drop/file-drop-collection-control.tsx +137 -0
  186. package/templates/ui/components/controls/file-drop/file-drop-control.test.tsx +189 -0
  187. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +81 -88
  188. package/templates/ui/components/controls/file-drop/file-drop-presentation.tsx +131 -9
  189. package/templates/ui/components/controls/file-drop/file-drop-types.ts +36 -22
  190. package/templates/ui/components/controls/file-drop/index.ts +1 -0
  191. package/templates/ui/components/controls/index.ts +3 -0
  192. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +1 -1
  193. package/templates/ui/components/controls/range-slider/range-slider-value.test.ts +14 -0
  194. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -2
  195. package/templates/starter/src/app/acceptance/condition-exclusivity.test.ts +0 -60
  196. package/templates/starter/src/app/acceptance/condition-exclusivity.ts +0 -221
@@ -1,4 +1,4 @@
1
- import type { ReactElement } from "react";
1
+ import type { ReactElement, ReactNode } from "react";
2
2
 
3
3
  import type {
4
4
  ActionControlIconName,
@@ -75,7 +75,20 @@ export type FileDropPresentation<
75
75
  status?: Status;
76
76
  };
77
77
 
78
+ export type FileDropCollectionActions = {
79
+ addLabel?: string;
80
+ canAdd?: boolean;
81
+ itemLabel?: string;
82
+ name: string;
83
+ removeLabel?: string;
84
+ renderItemContent?: (
85
+ item: FileDropPresentationItem,
86
+ index: number,
87
+ ) => ReactNode;
88
+ };
89
+
78
90
  type FileDropControlSharedProps<AssetKind extends string> = {
91
+ collectionActions?: FileDropCollectionActions;
79
92
  onClear?: () => void;
80
93
  onItemRemove?: (
81
94
  item: FileDropPresentationItem<AssetKind>,
@@ -108,27 +121,28 @@ type FileDropPresentationControlProps<
108
121
  previews?: never;
109
122
  };
110
123
 
111
- type FileDropLegacyControlProps = FileDropControlSharedProps<FileDropAssetKind> & {
112
- accept?: string;
113
- assetKind?: FileDropAssetKind;
114
- multiple?: boolean;
115
- onFileSelect?: (file: File) => void;
116
- onFilesSelect?: (files: File[]) => void;
117
- onFolderSelect?: never;
118
- onAction?: never;
119
- onItemRemove?: never;
120
- onItemSelect?: never;
121
- onItemsReorder?: never;
122
- onPreviewRemove?: (preview: FileDropPreview, index: number) => void;
123
- onPreviewReorder?: (orderedPreviews: FileDropPreview[]) => void;
124
- onPreviewTransform?: (
125
- preview: FileDropPreview,
126
- operation: FileDropImageTransformOperation,
127
- ) => void;
128
- presentation?: undefined;
129
- preview?: FileDropPreview;
130
- previews?: readonly FileDropPreview[];
131
- };
124
+ type FileDropLegacyControlProps =
125
+ FileDropControlSharedProps<FileDropAssetKind> & {
126
+ accept?: string;
127
+ assetKind?: FileDropAssetKind;
128
+ multiple?: boolean;
129
+ onFileSelect?: (file: File) => void;
130
+ onFilesSelect?: (files: File[]) => void;
131
+ onFolderSelect?: never;
132
+ onAction?: never;
133
+ onItemRemove?: never;
134
+ onItemSelect?: never;
135
+ onItemsReorder?: never;
136
+ onPreviewRemove?: (preview: FileDropPreview, index: number) => void;
137
+ onPreviewReorder?: (orderedPreviews: FileDropPreview[]) => void;
138
+ onPreviewTransform?: (
139
+ preview: FileDropPreview,
140
+ operation: FileDropImageTransformOperation,
141
+ ) => void;
142
+ presentation?: undefined;
143
+ preview?: FileDropPreview;
144
+ previews?: readonly FileDropPreview[];
145
+ };
132
146
 
133
147
  export type FileDropControlProps<
134
148
  AssetKind extends string = FileDropAssetKind,
@@ -3,6 +3,7 @@
3
3
  export { FileDropControl } from "./file-drop-control";
4
4
  export type {
5
5
  FileDropAssetKind,
6
+ FileDropCollectionActions,
6
7
  FileDropControlProps,
7
8
  FileDropImageSize,
8
9
  FileDropImageTransform,
@@ -89,10 +89,12 @@ export type {
89
89
  export {
90
90
  CollectionActionsControl,
91
91
  CollectionActionsControl as CollectionActions,
92
+ CollectionItemGroups,
92
93
  } from "./collection-actions";
93
94
  export type {
94
95
  CollectionActionsControlProps,
95
96
  CollectionActionsControlProps as CollectionActionsProps,
97
+ CollectionItemGroupsProps,
96
98
  } from "./collection-actions";
97
99
  export { createControlHistoryGroupId } from "./control-types";
98
100
  export type {
@@ -118,6 +120,7 @@ export type {
118
120
  export { FileDropControl, FileDropControl as FileDrop } from "./file-drop";
119
121
  export type {
120
122
  FileDropAssetKind,
123
+ FileDropCollectionActions,
121
124
  FileDropControlProps,
122
125
  FileDropControlProps as FileDropProps,
123
126
  FileDropImageSize,
@@ -52,7 +52,7 @@ export function RangeSliderControl({
52
52
  const liveHistoryGroupRef = React.useRef<string | null>(null);
53
53
  const resolvedValueLabel = valueLabel
54
54
  ? applySliderValueLabelUnit(valueLabel, unit)
55
- : formatRangeSliderValue(rangeValue, unit);
55
+ : formatRangeSliderValue(rangeValue, step, unit);
56
56
 
57
57
  React.useEffect(() => {
58
58
  setRangeValue(value);
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { formatRangeSliderValue } from "./range-slider-value";
4
+
5
+ describe("formatRangeSliderValue", () => {
6
+ it("preserves the precision declared by the slider step", () => {
7
+ expect(formatRangeSliderValue([0.25, 0.77], 0.01)).toBe("0.25 – 0.77");
8
+ expect(formatRangeSliderValue([2.5, 7.5], 0.1, "%")).toBe("2.5% – 7.5%");
9
+ });
10
+
11
+ it("keeps integer ranges compact", () => {
12
+ expect(formatRangeSliderValue([20, 80], 1)).toBe("20 – 80");
13
+ });
14
+ });
@@ -1,4 +1,4 @@
1
- import { applySliderValueLabelUnit } from "../slider/slider-value";
1
+ import { formatSliderValueWithUnit } from "../slider/slider-value";
2
2
 
3
3
  const signedNumberPattern = "[-−]?\\d+(?:[.,]\\d+)?";
4
4
  const rangePairPattern = new RegExp(
@@ -8,10 +8,11 @@ const rangePairPattern = new RegExp(
8
8
 
9
9
  export function formatRangeSliderValue(
10
10
  value: readonly number[],
11
+ step: number,
11
12
  unit?: string,
12
13
  ): string {
13
14
  const formatValue = (item: number): string =>
14
- applySliderValueLabelUnit(String(Math.round(item)), unit);
15
+ formatSliderValueWithUnit(item, step, unit);
15
16
 
16
17
  if (value.length >= 2 && value[0] === value[1]) {
17
18
  return formatValue(value[0] ?? 0);
@@ -1,60 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { areToolcraftConditionsProvablyExclusive } from "./condition-exclusivity";
4
-
5
- describe("Toolcraft condition exclusivity", () => {
6
- it("proves disjoint finite value gates", () => {
7
- expect(
8
- areToolcraftConditionsProvablyExclusive(
9
- { equals: "first", target: "model.active" },
10
- { equals: "second", target: "model.active" },
11
- ),
12
- ).toBe(true);
13
- expect(
14
- areToolcraftConditionsProvablyExclusive(
15
- { oneOf: ["first", "second"], target: "model.active" },
16
- { equals: "second", target: "model.active" },
17
- ),
18
- ).toBe(false);
19
- expect(
20
- areToolcraftConditionsProvablyExclusive(
21
- { notEquals: "hidden", target: "model.active" },
22
- { equals: "second", target: "model.active" },
23
- ),
24
- ).toBe(false);
25
- });
26
-
27
- it("proves non-overlapping numeric intervals without guessing across targets", () => {
28
- expect(
29
- areToolcraftConditionsProvablyExclusive(
30
- { lessThan: 0, target: "model.index" },
31
- { greaterThanOrEqual: 0, target: "model.index" },
32
- ),
33
- ).toBe(true);
34
- expect(
35
- areToolcraftConditionsProvablyExclusive(
36
- { equals: "first", target: "first.mode" },
37
- { equals: "second", target: "second.mode" },
38
- ),
39
- ).toBe(false);
40
- });
41
-
42
- it("stays conservative for primitive JSON collisions and invalid numeric bounds", () => {
43
- expect(
44
- areToolcraftConditionsProvablyExclusive(
45
- { equals: Number.NaN, target: "model.mode" },
46
- { notEquals: null, target: "model.mode" },
47
- ),
48
- ).toBe(false);
49
- expect(
50
- areToolcraftConditionsProvablyExclusive(
51
- {
52
- equals: 1,
53
- greaterThan: Number.NaN,
54
- target: "model.mode",
55
- },
56
- { equals: 1, target: "model.mode" },
57
- ),
58
- ).toBe(false);
59
- });
60
- });
@@ -1,221 +0,0 @@
1
- import type { ToolcraftControlConditionSchema } from "@repo/toolcraft-runtime";
2
-
3
- function valuesEqual(left: unknown, right: unknown): boolean {
4
- if (Object.is(left, right)) {
5
- return true;
6
- }
7
-
8
- if (
9
- (typeof left !== "object" || left === null) &&
10
- (typeof right !== "object" || right === null)
11
- ) {
12
- return false;
13
- }
14
-
15
- try {
16
- return JSON.stringify(left) === JSON.stringify(right);
17
- } catch {
18
- return false;
19
- }
20
- }
21
-
22
- function readComparableNumber(value: unknown): number | null {
23
- if (typeof value === "number" && Number.isFinite(value)) {
24
- return value;
25
- }
26
-
27
- if (typeof value === "string") {
28
- const numberValue = Number(value.trim());
29
-
30
- return Number.isFinite(numberValue) ? numberValue : null;
31
- }
32
-
33
- return null;
34
- }
35
-
36
- function conditionMatchesValue(
37
- condition: ToolcraftControlConditionSchema,
38
- value: unknown,
39
- ): boolean {
40
- const matches: boolean[] = [];
41
-
42
- if ("equals" in condition) {
43
- matches.push(valuesEqual(value, condition.equals));
44
- }
45
- if ("notEquals" in condition) {
46
- matches.push(!valuesEqual(value, condition.notEquals));
47
- }
48
- if (condition.oneOf) {
49
- matches.push(condition.oneOf.some((item) => valuesEqual(value, item)));
50
- }
51
- if (condition.notOneOf) {
52
- matches.push(!condition.notOneOf.some((item) => valuesEqual(value, item)));
53
- }
54
-
55
- const numberValue = readComparableNumber(value);
56
- for (const [expected, compare] of [
57
- [condition.greaterThan, (left: number, right: number) => left > right],
58
- [
59
- condition.greaterThanOrEqual,
60
- (left: number, right: number) => left >= right,
61
- ],
62
- [condition.lessThan, (left: number, right: number) => left < right],
63
- [
64
- condition.lessThanOrEqual,
65
- (left: number, right: number) => left <= right,
66
- ],
67
- ] as const) {
68
- if (typeof expected === "number" && Number.isFinite(expected)) {
69
- matches.push(numberValue !== null && compare(numberValue, expected));
70
- }
71
- }
72
-
73
- return matches.length > 0 && matches.every(Boolean);
74
- }
75
-
76
- function getFiniteConditionValues(
77
- condition: ToolcraftControlConditionSchema,
78
- ): readonly unknown[] | null {
79
- const candidates =
80
- "equals" in condition
81
- ? [condition.equals]
82
- : condition.oneOf
83
- ? [...condition.oneOf]
84
- : null;
85
-
86
- return candidates?.filter((value) => conditionMatchesValue(condition, value)) ?? null;
87
- }
88
-
89
- type NumericBoundary = {
90
- inclusive: boolean;
91
- value: number;
92
- };
93
-
94
- type NumericInterval = {
95
- lower: NumericBoundary | null;
96
- upper: NumericBoundary | null;
97
- };
98
-
99
- function stricterLowerBoundary(
100
- current: NumericBoundary | null,
101
- candidate: NumericBoundary,
102
- ): NumericBoundary {
103
- if (
104
- !current ||
105
- candidate.value > current.value ||
106
- (candidate.value === current.value && !candidate.inclusive)
107
- ) {
108
- return candidate;
109
- }
110
-
111
- return current;
112
- }
113
-
114
- function stricterUpperBoundary(
115
- current: NumericBoundary | null,
116
- candidate: NumericBoundary,
117
- ): NumericBoundary {
118
- if (
119
- !current ||
120
- candidate.value < current.value ||
121
- (candidate.value === current.value && !candidate.inclusive)
122
- ) {
123
- return candidate;
124
- }
125
-
126
- return current;
127
- }
128
-
129
- function getNumericInterval(
130
- condition: ToolcraftControlConditionSchema,
131
- ): NumericInterval | null {
132
- let lower: NumericBoundary | null = null;
133
- let upper: NumericBoundary | null = null;
134
-
135
- if (condition.greaterThan !== undefined) {
136
- lower = stricterLowerBoundary(lower, {
137
- inclusive: false,
138
- value: condition.greaterThan,
139
- });
140
- }
141
- if (condition.greaterThanOrEqual !== undefined) {
142
- lower = stricterLowerBoundary(lower, {
143
- inclusive: true,
144
- value: condition.greaterThanOrEqual,
145
- });
146
- }
147
- if (condition.lessThan !== undefined) {
148
- upper = stricterUpperBoundary(upper, {
149
- inclusive: false,
150
- value: condition.lessThan,
151
- });
152
- }
153
- if (condition.lessThanOrEqual !== undefined) {
154
- upper = stricterUpperBoundary(upper, {
155
- inclusive: true,
156
- value: condition.lessThanOrEqual,
157
- });
158
- }
159
-
160
- return lower || upper ? { lower, upper } : null;
161
- }
162
-
163
- function intervalEndsBefore(
164
- upper: NumericBoundary | null,
165
- lower: NumericBoundary | null,
166
- ): boolean {
167
- if (!upper || !lower) {
168
- return false;
169
- }
170
-
171
- return (
172
- upper.value < lower.value ||
173
- (upper.value === lower.value && (!upper.inclusive || !lower.inclusive))
174
- );
175
- }
176
-
177
- export function areToolcraftConditionsProvablyExclusive(
178
- left: ToolcraftControlConditionSchema,
179
- right: ToolcraftControlConditionSchema,
180
- ): boolean {
181
- if (left.target !== right.target) {
182
- return false;
183
- }
184
-
185
- const leftValues = getFiniteConditionValues(left);
186
- if (
187
- leftValues &&
188
- leftValues.every((value) => !conditionMatchesValue(right, value))
189
- ) {
190
- return true;
191
- }
192
-
193
- const rightValues = getFiniteConditionValues(right);
194
- if (
195
- rightValues &&
196
- rightValues.every((value) => !conditionMatchesValue(left, value))
197
- ) {
198
- return true;
199
- }
200
-
201
- const leftInterval = getNumericInterval(left);
202
- const rightInterval = getNumericInterval(right);
203
-
204
- return Boolean(
205
- leftInterval &&
206
- rightInterval &&
207
- (intervalEndsBefore(leftInterval.upper, rightInterval.lower) ||
208
- intervalEndsBefore(rightInterval.upper, leftInterval.lower)),
209
- );
210
- }
211
-
212
- export function areToolcraftConditionSetsProvablyExclusive(
213
- left: readonly ToolcraftControlConditionSchema[],
214
- right: readonly ToolcraftControlConditionSchema[],
215
- ): boolean {
216
- return left.some((leftCondition) =>
217
- right.some((rightCondition) =>
218
- areToolcraftConditionsProvablyExclusive(leftCondition, rightCondition),
219
- ),
220
- );
221
- }