@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
@@ -0,0 +1,321 @@
1
+ import { defineToolcraft } from "@repo/toolcraft-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import {
5
+ getToolcraftApplicabilityRequirementId,
6
+ getToolcraftControlApplicabilityCases,
7
+ } from "./control-applicability-cases";
8
+ import type { ToolcraftControlSectionInventoryEntry } from "./types";
9
+
10
+ const sectionInventory: readonly ToolcraftControlSectionInventoryEntry[] = [
11
+ {
12
+ entity: "Shape",
13
+ entityId: "shape",
14
+ groupingReason: "Source and shape selectors gate shape parameters.",
15
+ id: "shape",
16
+ targets: ["source.mode", "shape.kind", "shape.sides"],
17
+ title: "Shape",
18
+ },
19
+ ];
20
+
21
+ function shapeSchema({
22
+ applicability = {
23
+ all: [
24
+ { equals: "create", target: "source.mode" },
25
+ { oneOf: ["polygon", "star"], target: "shape.kind" },
26
+ ],
27
+ mode: "conditional" as const,
28
+ },
29
+ }: {
30
+ applicability?: {
31
+ all: readonly {
32
+ equals?: unknown;
33
+ oneOf?: readonly unknown[];
34
+ target: string;
35
+ }[];
36
+ mode: "conditional";
37
+ } | { mode: "always" };
38
+ } = {}) {
39
+ return defineToolcraft({
40
+ canvas: { enabled: true },
41
+ panels: {
42
+ controls: {
43
+ sections: [
44
+ {
45
+ controls: {
46
+ sourceMode: {
47
+ applicability: { mode: "always" },
48
+ defaultValue: "create",
49
+ options: [
50
+ { label: "Upload", value: "upload" },
51
+ { label: "Create", value: "create" },
52
+ ],
53
+ target: "source.mode",
54
+ type: "segmented",
55
+ },
56
+ shapeKind: {
57
+ applicability: { mode: "always" },
58
+ defaultValue: "polygon",
59
+ options: [
60
+ { label: "Circle", value: "circle" },
61
+ { label: "Polygon", value: "polygon" },
62
+ { label: "Star", value: "star" },
63
+ ],
64
+ target: "shape.kind",
65
+ type: "segmented",
66
+ },
67
+ sides: {
68
+ applicability,
69
+ defaultValue: 6,
70
+ target: "shape.sides",
71
+ type: "slider",
72
+ },
73
+ },
74
+ id: "shape",
75
+ title: "Shape",
76
+ },
77
+ ],
78
+ title: "Controls",
79
+ },
80
+ },
81
+ });
82
+ }
83
+
84
+ describe("Toolcraft control applicability cases", () => {
85
+ it("derives deterministic pairwise cases from semantic inventory", () => {
86
+ const cases = getToolcraftControlApplicabilityCases({
87
+ schema: shapeSchema(),
88
+ sectionInventory,
89
+ target: "shape.sides",
90
+ });
91
+
92
+ expect(cases.map(({ expectation, selectorTarget, selectorValue, target }) => ({
93
+ expectation,
94
+ selectorTarget,
95
+ selectorValue,
96
+ target,
97
+ }))).toEqual([
98
+ {
99
+ expectation: "hidden",
100
+ selectorTarget: "source.mode",
101
+ selectorValue: "upload",
102
+ target: "shape.sides",
103
+ },
104
+ {
105
+ expectation: "visible",
106
+ selectorTarget: "source.mode",
107
+ selectorValue: "create",
108
+ target: "shape.sides",
109
+ },
110
+ {
111
+ expectation: "hidden",
112
+ selectorTarget: "shape.kind",
113
+ selectorValue: "circle",
114
+ target: "shape.sides",
115
+ },
116
+ {
117
+ expectation: "visible",
118
+ selectorTarget: "shape.kind",
119
+ selectorValue: "polygon",
120
+ target: "shape.sides",
121
+ },
122
+ {
123
+ expectation: "visible",
124
+ selectorTarget: "shape.kind",
125
+ selectorValue: "star",
126
+ target: "shape.sides",
127
+ },
128
+ ]);
129
+ expect(cases[0]).toEqual(
130
+ expect.objectContaining({
131
+ selectorControlType: "segmented",
132
+ selectorLabel: "sourceMode",
133
+ selectorOptionLabel: "Upload",
134
+ }),
135
+ );
136
+ });
137
+
138
+ it("treats omitted finite siblings as visible claims", () => {
139
+ const cases = getToolcraftControlApplicabilityCases({
140
+ schema: shapeSchema({
141
+ applicability: {
142
+ all: [{ oneOf: ["polygon", "star"], target: "shape.kind" }],
143
+ mode: "conditional",
144
+ },
145
+ }),
146
+ sectionInventory,
147
+ target: "shape.sides",
148
+ });
149
+
150
+ expect(cases.slice(0, 2)).toEqual([
151
+ expect.objectContaining({
152
+ expectation: "visible",
153
+ selectorTarget: "source.mode",
154
+ selectorValue: "upload",
155
+ target: "shape.sides",
156
+ }),
157
+ expect.objectContaining({
158
+ expectation: "visible",
159
+ selectorTarget: "source.mode",
160
+ selectorValue: "create",
161
+ target: "shape.sides",
162
+ }),
163
+ ]);
164
+ });
165
+
166
+ it("uses one path for always controls and stable encoded requirement ids", () => {
167
+ const cases = getToolcraftControlApplicabilityCases({
168
+ schema: shapeSchema({ applicability: { mode: "always" } }),
169
+ sectionInventory,
170
+ target: "shape.sides",
171
+ });
172
+
173
+ expect(cases.every((entry) => entry.expectation === "visible")).toBe(true);
174
+ expect(getToolcraftApplicabilityRequirementId("shape-sides", cases[0]!)).toBe(
175
+ "shape-sides#applicability:source.mode=%22upload%22:visible",
176
+ );
177
+ });
178
+
179
+ it("derives bounded numeric boundary cases without enumerating a range", () => {
180
+ const schema = defineToolcraft({
181
+ canvas: { enabled: true },
182
+ panels: {
183
+ controls: {
184
+ sections: [
185
+ {
186
+ controls: {
187
+ count: {
188
+ applicability: { mode: "always" },
189
+ defaultValue: 2,
190
+ max: 3,
191
+ min: 1,
192
+ step: 1,
193
+ target: "shade.count",
194
+ type: "slider",
195
+ variant: "discrete",
196
+ },
197
+ thirdShade: {
198
+ applicability: {
199
+ all: [
200
+ {
201
+ greaterThanOrEqual: 3,
202
+ target: "shade.count",
203
+ },
204
+ ],
205
+ mode: "conditional",
206
+ },
207
+ target: "shade.third",
208
+ type: "color",
209
+ },
210
+ },
211
+ id: "shades",
212
+ title: "Shades",
213
+ },
214
+ ],
215
+ title: "Controls",
216
+ },
217
+ },
218
+ });
219
+
220
+ expect(
221
+ getToolcraftControlApplicabilityCases({
222
+ schema,
223
+ sectionInventory: [
224
+ {
225
+ entity: "Shades",
226
+ entityId: "shades",
227
+ groupingReason: "Count controls the available shade colors.",
228
+ id: "shades",
229
+ targets: ["shade.count", "shade.third"],
230
+ title: "Shades",
231
+ },
232
+ ],
233
+ target: "shade.third",
234
+ }).map(({ expectation, selectorTarget, selectorValue, target }) => ({
235
+ expectation,
236
+ selectorTarget,
237
+ selectorValue,
238
+ target,
239
+ })),
240
+ ).toEqual([
241
+ {
242
+ expectation: "hidden",
243
+ selectorTarget: "shade.count",
244
+ selectorValue: 1,
245
+ target: "shade.third",
246
+ },
247
+ {
248
+ expectation: "hidden",
249
+ selectorTarget: "shade.count",
250
+ selectorValue: 2,
251
+ target: "shade.third",
252
+ },
253
+ {
254
+ expectation: "visible",
255
+ selectorTarget: "shade.count",
256
+ selectorValue: 3,
257
+ target: "shade.third",
258
+ },
259
+ ]);
260
+ });
261
+
262
+ it("uses the resolved Setup product subset only as the inventory fallback", () => {
263
+ const schema = defineToolcraft({
264
+ canvas: {
265
+ enabled: true,
266
+ sizing: { mode: "editable-output" },
267
+ },
268
+ panels: {
269
+ controls: {
270
+ sections: [
271
+ {
272
+ controls: {
273
+ background: {
274
+ applicability: { mode: "always" },
275
+ defaultValue: "#101010",
276
+ target: "appearance.background",
277
+ type: "color",
278
+ },
279
+ includeBackground: {
280
+ applicability: { mode: "always" },
281
+ defaultValue: true,
282
+ target: "export.includeBackground",
283
+ type: "switch",
284
+ },
285
+ },
286
+ id: "background",
287
+ title: "Background",
288
+ },
289
+ ],
290
+ title: "Controls",
291
+ },
292
+ },
293
+ });
294
+
295
+ expect(
296
+ getToolcraftControlApplicabilityCases({
297
+ schema,
298
+ sectionInventory: [],
299
+ target: "appearance.background",
300
+ }).map(({ expectation, selectorTarget, selectorValue, target }) => ({
301
+ expectation,
302
+ selectorTarget,
303
+ selectorValue,
304
+ target,
305
+ })),
306
+ ).toEqual([
307
+ {
308
+ expectation: "visible",
309
+ selectorTarget: "export.includeBackground",
310
+ selectorValue: false,
311
+ target: "appearance.background",
312
+ },
313
+ {
314
+ expectation: "visible",
315
+ selectorTarget: "export.includeBackground",
316
+ selectorValue: true,
317
+ target: "appearance.background",
318
+ },
319
+ ]);
320
+ });
321
+ });
@@ -0,0 +1,290 @@
1
+ import {
2
+ doesToolcraftApplicabilityMatch,
3
+ doesToolcraftPredicateMatchValue,
4
+ getToolcraftApplicabilityPredicates,
5
+ type ResolvedToolcraftAppSchema,
6
+ type ResolvedToolcraftControlSchema,
7
+ type ToolcraftControlPredicateSchema,
8
+ } from "@repo/toolcraft-runtime";
9
+
10
+ import { getToolcraftApplicabilitySelectorDomain } from "./control-applicability";
11
+ import {
12
+ isToolcraftProductSectionControl,
13
+ isToolcraftVisibleAcceptanceControl,
14
+ } from "./controls";
15
+ import type { ToolcraftControlSectionInventoryEntry } from "./types";
16
+
17
+ export type ToolcraftControlApplicabilityCase = Readonly<{
18
+ expectation: "hidden" | "visible";
19
+ selectorControlType:
20
+ | "checkbox"
21
+ | "imagePicker"
22
+ | "segmented"
23
+ | "select"
24
+ | "slider"
25
+ | "switch"
26
+ | "tabs";
27
+ selectorLabel: string;
28
+ selectorOptionLabel?: string;
29
+ selectorTarget: string;
30
+ selectorValue: boolean | number | string;
31
+ target: string;
32
+ }>;
33
+
34
+ type IndexedControl = Readonly<{
35
+ control: ResolvedToolcraftControlSchema;
36
+ id: string;
37
+ sectionId: string;
38
+ }>;
39
+
40
+ type ApplicabilitySelectorControlType =
41
+ ToolcraftControlApplicabilityCase["selectorControlType"];
42
+
43
+ function indexControls(
44
+ schema: Pick<ResolvedToolcraftAppSchema, "panels">,
45
+ ): Map<string, IndexedControl> {
46
+ const controls = new Map<string, IndexedControl>();
47
+
48
+ for (const section of schema.panels.controls?.sections ?? []) {
49
+ for (const [id, control] of Object.entries(section.controls)) {
50
+ controls.set(control.target, { control, id, sectionId: section.id });
51
+ }
52
+ }
53
+
54
+ return controls;
55
+ }
56
+
57
+ function getApplicabilitySelectorControlType(
58
+ control: ResolvedToolcraftControlSchema,
59
+ ): ApplicabilitySelectorControlType | null {
60
+ switch (control.type) {
61
+ case "checkbox":
62
+ case "imagePicker":
63
+ case "segmented":
64
+ case "select":
65
+ case "slider":
66
+ case "switch":
67
+ case "tabs":
68
+ return control.type;
69
+ default:
70
+ return null;
71
+ }
72
+ }
73
+
74
+ function getSelectorOptionLabel(
75
+ control: ResolvedToolcraftControlSchema,
76
+ value: boolean | number | string,
77
+ ): string | undefined {
78
+ if (
79
+ control.type === "select" ||
80
+ control.type === "segmented" ||
81
+ control.type === "tabs"
82
+ ) {
83
+ return control.options?.find((option) => option.value === value)?.label;
84
+ }
85
+
86
+ if (control.type === "imagePicker") {
87
+ const item = control.items?.find((candidate) => candidate.value === value);
88
+ return item ? (item.alt ?? item.value) : undefined;
89
+ }
90
+
91
+ return undefined;
92
+ }
93
+
94
+ function getSemanticPeerTargets({
95
+ dependent,
96
+ sectionInventory,
97
+ schema,
98
+ }: {
99
+ dependent: IndexedControl;
100
+ sectionInventory: readonly ToolcraftControlSectionInventoryEntry[];
101
+ schema: Pick<ResolvedToolcraftAppSchema, "panels">;
102
+ }): readonly string[] {
103
+ const inventoryEntry = sectionInventory.find((entry) =>
104
+ entry.targets.includes(dependent.control.target),
105
+ );
106
+
107
+ if (inventoryEntry) {
108
+ return inventoryEntry.targets;
109
+ }
110
+
111
+ if (dependent.sectionId !== "runtime.setup") {
112
+ return [];
113
+ }
114
+
115
+ const setupSection = schema.panels.controls?.sections.find(
116
+ (section) => section.id === "runtime.setup",
117
+ );
118
+
119
+ return Object.values(setupSection?.controls ?? {})
120
+ .filter(isToolcraftProductSectionControl)
121
+ .map((control) => control.target);
122
+ }
123
+
124
+ function getPredicatesTargeting(
125
+ controlsByTarget: ReadonlyMap<string, IndexedControl>,
126
+ peerTargets: readonly string[],
127
+ selectorTarget: string,
128
+ ): ToolcraftControlPredicateSchema[] {
129
+ return peerTargets.flatMap((target) => {
130
+ const applicabilityControl = controlsByTarget.get(target)?.control;
131
+ const applicability = applicabilityControl?.applicability;
132
+
133
+ return applicability
134
+ ? getToolcraftApplicabilityPredicates(applicability).filter(
135
+ (predicate) => predicate.target === selectorTarget,
136
+ )
137
+ : [];
138
+ });
139
+ }
140
+
141
+ function getSatisfyingBaseline(
142
+ dependent: ResolvedToolcraftControlSchema,
143
+ controlsByTarget: ReadonlyMap<string, IndexedControl>,
144
+ ): Map<string, boolean | number | string> {
145
+ const baseline = new Map<string, boolean | number | string>();
146
+ const predicates = getToolcraftApplicabilityPredicates(
147
+ dependent.applicability,
148
+ );
149
+ const predicatesByTarget = new Map<string, ToolcraftControlPredicateSchema[]>();
150
+
151
+ for (const predicate of predicates) {
152
+ const targetPredicates = predicatesByTarget.get(predicate.target) ?? [];
153
+ targetPredicates.push(predicate);
154
+ predicatesByTarget.set(predicate.target, targetPredicates);
155
+ }
156
+
157
+ for (const [target, targetPredicates] of predicatesByTarget) {
158
+ const selector = controlsByTarget.get(target)?.control;
159
+
160
+ if (!selector) {
161
+ continue;
162
+ }
163
+
164
+ const domain = getToolcraftApplicabilitySelectorDomain(
165
+ selector,
166
+ targetPredicates,
167
+ );
168
+
169
+ if (domain.status !== "supported") {
170
+ continue;
171
+ }
172
+
173
+ const value = domain.values.find((candidate) =>
174
+ targetPredicates.every((predicate) =>
175
+ doesToolcraftPredicateMatchValue(predicate, candidate),
176
+ ),
177
+ );
178
+
179
+ if (value !== undefined) {
180
+ baseline.set(target, value);
181
+ }
182
+ }
183
+
184
+ return baseline;
185
+ }
186
+
187
+ export function getToolcraftControlApplicabilityCases(
188
+ input: Readonly<{
189
+ schema: Pick<ResolvedToolcraftAppSchema, "panels">;
190
+ sectionInventory: readonly ToolcraftControlSectionInventoryEntry[];
191
+ target: string;
192
+ }>,
193
+ ): ToolcraftControlApplicabilityCase[] {
194
+ const controlsByTarget = indexControls(input.schema);
195
+ const dependent = controlsByTarget.get(input.target);
196
+
197
+ if (
198
+ !dependent ||
199
+ !isToolcraftVisibleAcceptanceControl(dependent.control)
200
+ ) {
201
+ return [];
202
+ }
203
+
204
+ const peerTargets = getSemanticPeerTargets({
205
+ dependent,
206
+ schema: input.schema,
207
+ sectionInventory: input.sectionInventory,
208
+ });
209
+ const baseline = getSatisfyingBaseline(dependent.control, controlsByTarget);
210
+ const cases: ToolcraftControlApplicabilityCase[] = [];
211
+
212
+ for (const selectorTarget of peerTargets) {
213
+ if (selectorTarget === dependent.control.target) {
214
+ continue;
215
+ }
216
+
217
+ const indexedSelector = controlsByTarget.get(selectorTarget);
218
+
219
+ if (!indexedSelector) {
220
+ continue;
221
+ }
222
+ const { control: selector } = indexedSelector;
223
+
224
+ const peerPredicates = getPredicatesTargeting(
225
+ controlsByTarget,
226
+ peerTargets,
227
+ selectorTarget,
228
+ );
229
+
230
+ if (selector.type === "slider" && peerPredicates.length === 0) {
231
+ continue;
232
+ }
233
+
234
+ const domain = getToolcraftApplicabilitySelectorDomain(
235
+ selector,
236
+ peerPredicates,
237
+ );
238
+
239
+ if (domain.status !== "supported") {
240
+ continue;
241
+ }
242
+
243
+ const selectorControlType = getApplicabilitySelectorControlType(selector);
244
+
245
+ if (!selectorControlType) {
246
+ continue;
247
+ }
248
+
249
+ for (const selectorValue of domain.values) {
250
+ const values = new Map(baseline);
251
+ values.set(selectorTarget, selectorValue);
252
+ const selectorOptionLabel = getSelectorOptionLabel(
253
+ selector,
254
+ selectorValue,
255
+ );
256
+ const matches = doesToolcraftApplicabilityMatch(
257
+ dependent.control.applicability,
258
+ (target) =>
259
+ values.get(target) ??
260
+ controlsByTarget.get(target)?.control.defaultValue,
261
+ );
262
+
263
+ cases.push({
264
+ expectation: matches ? "visible" : "hidden",
265
+ selectorControlType,
266
+ selectorLabel:
267
+ typeof selector.label === "string"
268
+ ? selector.label
269
+ : indexedSelector.id,
270
+ ...(selectorOptionLabel ? { selectorOptionLabel } : {}),
271
+ selectorTarget,
272
+ selectorValue,
273
+ target: dependent.control.target,
274
+ });
275
+ }
276
+ }
277
+
278
+ return cases;
279
+ }
280
+
281
+ export function getToolcraftApplicabilityRequirementId(
282
+ baseRequirementId: string,
283
+ applicabilityCase: ToolcraftControlApplicabilityCase,
284
+ ): string {
285
+ const serializedValue =
286
+ JSON.stringify(applicabilityCase.selectorValue) ??
287
+ String(applicabilityCase.selectorValue);
288
+
289
+ return `${baseRequirementId}#applicability:${applicabilityCase.selectorTarget}=${encodeURIComponent(serializedValue)}:${applicabilityCase.expectation}`;
290
+ }