@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
@@ -45,6 +45,10 @@ describe("resolveToolcraftAppCapabilities", () => {
45
45
  {
46
46
  controls: {
47
47
  source: {
48
+ applicability: {
49
+ mode: "always",
50
+ origin: "implicit",
51
+ },
48
52
  assetKind: "file",
49
53
  target: "source.files",
50
54
  type: "fileDrop",
@@ -0,0 +1,29 @@
1
+ export const TOOLCRAFT_COLLECTION_ITEM_CONTROL_TYPES = [
2
+ "checkbox",
3
+ "color",
4
+ "colorOpacity",
5
+ "fontPicker",
6
+ "rangeInput",
7
+ "rangeSlider",
8
+ "segmented",
9
+ "select",
10
+ "slider",
11
+ "switch",
12
+ "text",
13
+ "vector",
14
+ ] as const;
15
+
16
+ export type ToolcraftCollectionItemControlType =
17
+ (typeof TOOLCRAFT_COLLECTION_ITEM_CONTROL_TYPES)[number];
18
+
19
+ const toolcraftCollectionItemControlTypes = new Set<string>(
20
+ TOOLCRAFT_COLLECTION_ITEM_CONTROL_TYPES,
21
+ );
22
+
23
+ export function isToolcraftCollectionItemControlType(
24
+ value: unknown,
25
+ ): value is ToolcraftCollectionItemControlType {
26
+ return (
27
+ typeof value === "string" && toolcraftCollectionItemControlTypes.has(value)
28
+ );
29
+ }
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ areToolcraftPredicateSetsProvablyExclusive,
5
+ areToolcraftPredicatesProvablyExclusive,
6
+ doesToolcraftApplicabilityMatch,
7
+ doesToolcraftPredicateMatchValue,
8
+ getToolcraftApplicabilityTargets,
9
+ resolveToolcraftControlApplicability,
10
+ } from "./control-applicability";
11
+
12
+ describe("Toolcraft control applicability", () => {
13
+ it("normalizes explicit, legacy, and implicit inputs into one model", () => {
14
+ expect(
15
+ resolveToolcraftControlApplicability({
16
+ applicability: { mode: "always" },
17
+ }),
18
+ ).toEqual({ mode: "always", origin: "explicit" });
19
+
20
+ expect(
21
+ resolveToolcraftControlApplicability({
22
+ applicability: {
23
+ all: [
24
+ { equals: "create", target: "source.mode" },
25
+ { oneOf: ["polygon", "star"], target: "shape.kind" },
26
+ ],
27
+ mode: "conditional",
28
+ },
29
+ }),
30
+ ).toEqual({
31
+ all: [
32
+ { equals: "create", target: "source.mode" },
33
+ { oneOf: ["polygon", "star"], target: "shape.kind" },
34
+ ],
35
+ mode: "conditional",
36
+ origin: "explicit",
37
+ });
38
+
39
+ expect(
40
+ resolveToolcraftControlApplicability({
41
+ visibleWhen: { equals: "create", target: "source.mode" },
42
+ }),
43
+ ).toEqual({
44
+ all: [{ equals: "create", target: "source.mode" }],
45
+ mode: "conditional",
46
+ origin: "legacy",
47
+ });
48
+
49
+ expect(resolveToolcraftControlApplicability({})).toEqual({
50
+ mode: "always",
51
+ origin: "implicit",
52
+ });
53
+
54
+ expect(
55
+ resolveToolcraftControlApplicability({
56
+ applicability: { mode: "always", origin: "implicit" },
57
+ }),
58
+ ).toEqual({ mode: "always", origin: "implicit" });
59
+ });
60
+
61
+ it("rejects ambiguous and structurally invalid input", () => {
62
+ expect(() =>
63
+ resolveToolcraftControlApplicability({
64
+ applicability: { mode: "always" },
65
+ visibleWhen: { equals: true, target: "feature.enabled" },
66
+ }),
67
+ ).toThrow("cannot declare both applicability and visibleWhen");
68
+
69
+ expect(() =>
70
+ resolveToolcraftControlApplicability({
71
+ applicability: { all: [], mode: "conditional" },
72
+ }),
73
+ ).toThrow("conditional applicability requires at least one predicate");
74
+
75
+ expect(() =>
76
+ resolveToolcraftControlApplicability({
77
+ applicability: { mode: "always", origin: "legacy" },
78
+ }),
79
+ ).toThrow("invalid resolved applicability origin");
80
+ });
81
+
82
+ it("evaluates predicates once and combines targets with AND", () => {
83
+ const applicability = {
84
+ all: [
85
+ { equals: "create", target: "source.mode" },
86
+ { oneOf: ["polygon", "star"], target: "shape.kind" },
87
+ ],
88
+ mode: "conditional",
89
+ origin: "explicit",
90
+ } as const;
91
+ const values = new Map<string, unknown>([
92
+ ["source.mode", "create"],
93
+ ["shape.kind", "star"],
94
+ ]);
95
+
96
+ expect(getToolcraftApplicabilityTargets(applicability)).toEqual([
97
+ "source.mode",
98
+ "shape.kind",
99
+ ]);
100
+ expect(
101
+ doesToolcraftApplicabilityMatch(applicability, (target) =>
102
+ values.get(target),
103
+ ),
104
+ ).toBe(true);
105
+ expect(
106
+ doesToolcraftPredicateMatchValue(
107
+ { equals: "heart", target: "shape.kind" },
108
+ "star",
109
+ ),
110
+ ).toBe(false);
111
+ });
112
+
113
+ it("proves finite and numeric predicate exclusivity", () => {
114
+ expect(
115
+ areToolcraftPredicatesProvablyExclusive(
116
+ { equals: "circle", target: "shape.kind" },
117
+ { oneOf: ["polygon", "star"], target: "shape.kind" },
118
+ ),
119
+ ).toBe(true);
120
+ expect(
121
+ areToolcraftPredicateSetsProvablyExclusive(
122
+ [{ greaterThan: 5, target: "shape.count" }],
123
+ [{ lessThanOrEqual: 5, target: "shape.count" }],
124
+ ),
125
+ ).toBe(true);
126
+ });
127
+ });
@@ -0,0 +1,433 @@
1
+ import type {
2
+ ToolcraftControlPredicateSchema,
3
+ ToolcraftResolvedControlApplicabilitySchema,
4
+ } from "./types";
5
+
6
+ export type ToolcraftControlApplicabilityNormalizationSchema =
7
+ | Readonly<{
8
+ mode: "always";
9
+ origin?: "explicit" | "implicit" | "legacy";
10
+ }>
11
+ | Readonly<{
12
+ all: readonly ToolcraftControlPredicateSchema[];
13
+ mode: "conditional";
14
+ origin?: "explicit" | "implicit" | "legacy";
15
+ }>;
16
+
17
+ export type ToolcraftControlApplicabilityNormalizationInput = Readonly<{
18
+ applicability?: ToolcraftControlApplicabilityNormalizationSchema;
19
+ visibleWhen?: ToolcraftControlPredicateSchema;
20
+ }>;
21
+
22
+ const numericOperatorNames = [
23
+ "greaterThan",
24
+ "greaterThanOrEqual",
25
+ "lessThan",
26
+ "lessThanOrEqual",
27
+ ] as const;
28
+
29
+ const valueOperatorNames = [
30
+ "equals",
31
+ "notEquals",
32
+ "oneOf",
33
+ "notOneOf",
34
+ ] as const;
35
+
36
+ function hasOwn(
37
+ value: object,
38
+ key: PropertyKey,
39
+ ): boolean {
40
+ return Object.prototype.hasOwnProperty.call(value, key);
41
+ }
42
+
43
+ export function areToolcraftValuesEqual(
44
+ left: unknown,
45
+ right: unknown,
46
+ ): boolean {
47
+ if (Object.is(left, right)) {
48
+ return true;
49
+ }
50
+
51
+ if (
52
+ (typeof left !== "object" || left === null) &&
53
+ (typeof right !== "object" || right === null)
54
+ ) {
55
+ return false;
56
+ }
57
+
58
+ try {
59
+ return JSON.stringify(left) === JSON.stringify(right);
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ function readComparableNumber(value: unknown): number | null {
66
+ if (typeof value === "number" && Number.isFinite(value)) {
67
+ return value;
68
+ }
69
+
70
+ if (typeof value === "string") {
71
+ const numberValue = Number(value.trim());
72
+
73
+ return Number.isFinite(numberValue) ? numberValue : null;
74
+ }
75
+
76
+ return null;
77
+ }
78
+
79
+ function assertPredicate(
80
+ predicate: ToolcraftControlPredicateSchema,
81
+ index: number,
82
+ ): void {
83
+ if (
84
+ !predicate ||
85
+ typeof predicate !== "object" ||
86
+ typeof predicate.target !== "string" ||
87
+ predicate.target.trim().length === 0
88
+ ) {
89
+ throw new Error(
90
+ `Toolcraft control applicability predicate ${index} requires a non-empty target.`,
91
+ );
92
+ }
93
+
94
+ const hasValueOperator = valueOperatorNames.some((operator) =>
95
+ hasOwn(predicate, operator),
96
+ );
97
+ const hasNumericOperator = numericOperatorNames.some((operator) =>
98
+ hasOwn(predicate, operator),
99
+ );
100
+
101
+ if (!hasValueOperator && !hasNumericOperator) {
102
+ throw new Error(
103
+ `Toolcraft control applicability predicate ${index} requires at least one operator.`,
104
+ );
105
+ }
106
+
107
+ for (const operator of ["oneOf", "notOneOf"] as const) {
108
+ if (
109
+ hasOwn(predicate, operator) &&
110
+ (!Array.isArray(predicate[operator]) || predicate[operator].length === 0)
111
+ ) {
112
+ throw new Error(
113
+ `Toolcraft control applicability predicate ${index} ${operator} requires at least one value.`,
114
+ );
115
+ }
116
+ }
117
+
118
+ for (const operator of numericOperatorNames) {
119
+ if (
120
+ hasOwn(predicate, operator) &&
121
+ (typeof predicate[operator] !== "number" ||
122
+ !Number.isFinite(predicate[operator]))
123
+ ) {
124
+ throw new Error(
125
+ `Toolcraft control applicability predicate ${index} ${operator} requires a finite number.`,
126
+ );
127
+ }
128
+ }
129
+ }
130
+
131
+ function clonePredicate(
132
+ predicate: ToolcraftControlPredicateSchema,
133
+ ): ToolcraftControlPredicateSchema {
134
+ return {
135
+ ...predicate,
136
+ ...(predicate.oneOf ? { oneOf: [...predicate.oneOf] } : {}),
137
+ ...(predicate.notOneOf ? { notOneOf: [...predicate.notOneOf] } : {}),
138
+ };
139
+ }
140
+
141
+ export function resolveToolcraftControlApplicability(
142
+ input: ToolcraftControlApplicabilityNormalizationInput,
143
+ ): ToolcraftResolvedControlApplicabilitySchema {
144
+ if (input.applicability && input.visibleWhen) {
145
+ throw new Error(
146
+ "Toolcraft controls cannot declare both applicability and visibleWhen.",
147
+ );
148
+ }
149
+
150
+ if (input.visibleWhen) {
151
+ assertPredicate(input.visibleWhen, 0);
152
+ return {
153
+ all: [clonePredicate(input.visibleWhen)],
154
+ mode: "conditional",
155
+ origin: "legacy",
156
+ };
157
+ }
158
+
159
+ const applicability = input.applicability;
160
+
161
+ if (!applicability) {
162
+ return { mode: "always", origin: "implicit" };
163
+ }
164
+
165
+ if (applicability.mode === "always") {
166
+ const origin = applicability.origin ?? "explicit";
167
+
168
+ if (origin !== "explicit" && origin !== "implicit") {
169
+ throw new Error(
170
+ `Toolcraft control applicability has invalid resolved applicability origin "${origin}" for mode "always".`,
171
+ );
172
+ }
173
+
174
+ return { mode: "always", origin };
175
+ }
176
+
177
+ if (!Array.isArray(applicability.all) || applicability.all.length === 0) {
178
+ throw new Error(
179
+ "Toolcraft conditional applicability requires at least one predicate.",
180
+ );
181
+ }
182
+
183
+ const origin = applicability.origin ?? "explicit";
184
+
185
+ if (origin !== "explicit" && origin !== "legacy") {
186
+ throw new Error(
187
+ `Toolcraft control applicability has invalid resolved applicability origin "${origin}" for mode "conditional".`,
188
+ );
189
+ }
190
+
191
+ const all = applicability.all.map((predicate, index) => {
192
+ assertPredicate(predicate, index);
193
+ return clonePredicate(predicate);
194
+ });
195
+
196
+ return { all, mode: "conditional", origin };
197
+ }
198
+
199
+ export function doesToolcraftPredicateMatchValue(
200
+ predicate: ToolcraftControlPredicateSchema,
201
+ value: unknown,
202
+ ): boolean {
203
+ const matches: boolean[] = [];
204
+
205
+ if (hasOwn(predicate, "equals")) {
206
+ matches.push(areToolcraftValuesEqual(value, predicate.equals));
207
+ }
208
+ if (hasOwn(predicate, "notEquals")) {
209
+ matches.push(!areToolcraftValuesEqual(value, predicate.notEquals));
210
+ }
211
+ if (Array.isArray(predicate.oneOf)) {
212
+ matches.push(
213
+ predicate.oneOf.some((candidate) =>
214
+ areToolcraftValuesEqual(value, candidate),
215
+ ),
216
+ );
217
+ }
218
+ if (Array.isArray(predicate.notOneOf)) {
219
+ matches.push(
220
+ !predicate.notOneOf.some((candidate) =>
221
+ areToolcraftValuesEqual(value, candidate),
222
+ ),
223
+ );
224
+ }
225
+
226
+ const numberValue = readComparableNumber(value);
227
+
228
+ for (const [expected, compare] of [
229
+ [predicate.greaterThan, (left: number, right: number) => left > right],
230
+ [
231
+ predicate.greaterThanOrEqual,
232
+ (left: number, right: number) => left >= right,
233
+ ],
234
+ [predicate.lessThan, (left: number, right: number) => left < right],
235
+ [
236
+ predicate.lessThanOrEqual,
237
+ (left: number, right: number) => left <= right,
238
+ ],
239
+ ] as const) {
240
+ if (typeof expected === "number" && Number.isFinite(expected)) {
241
+ matches.push(numberValue !== null && compare(numberValue, expected));
242
+ }
243
+ }
244
+
245
+ return matches.length > 0 && matches.every(Boolean);
246
+ }
247
+
248
+ export function doesToolcraftApplicabilityMatch(
249
+ applicability: ToolcraftResolvedControlApplicabilitySchema,
250
+ readTarget: (target: string) => unknown,
251
+ ): boolean {
252
+ return (
253
+ applicability.mode === "always" ||
254
+ applicability.all.every((predicate) =>
255
+ doesToolcraftPredicateMatchValue(
256
+ predicate,
257
+ readTarget(predicate.target),
258
+ ),
259
+ )
260
+ );
261
+ }
262
+
263
+ export function getToolcraftApplicabilityPredicates(
264
+ applicability: ToolcraftResolvedControlApplicabilitySchema,
265
+ ): readonly ToolcraftControlPredicateSchema[] {
266
+ return applicability.mode === "conditional" ? applicability.all : [];
267
+ }
268
+
269
+ export function getToolcraftApplicabilityTargets(
270
+ applicability: ToolcraftResolvedControlApplicabilitySchema,
271
+ ): string[] {
272
+ return [
273
+ ...new Set(
274
+ getToolcraftApplicabilityPredicates(applicability).map(
275
+ (predicate) => predicate.target,
276
+ ),
277
+ ),
278
+ ];
279
+ }
280
+
281
+ function getFinitePredicateValues(
282
+ predicate: ToolcraftControlPredicateSchema,
283
+ ): readonly unknown[] | null {
284
+ const candidates = hasOwn(predicate, "equals")
285
+ ? [predicate.equals]
286
+ : predicate.oneOf
287
+ ? [...predicate.oneOf]
288
+ : null;
289
+
290
+ return (
291
+ candidates?.filter((value) =>
292
+ doesToolcraftPredicateMatchValue(predicate, value),
293
+ ) ?? null
294
+ );
295
+ }
296
+
297
+ type NumericBoundary = Readonly<{
298
+ inclusive: boolean;
299
+ value: number;
300
+ }>;
301
+
302
+ type NumericInterval = Readonly<{
303
+ lower: NumericBoundary | null;
304
+ upper: NumericBoundary | null;
305
+ }>;
306
+
307
+ function stricterLowerBoundary(
308
+ current: NumericBoundary | null,
309
+ candidate: NumericBoundary,
310
+ ): NumericBoundary {
311
+ if (
312
+ !current ||
313
+ candidate.value > current.value ||
314
+ (candidate.value === current.value && !candidate.inclusive)
315
+ ) {
316
+ return candidate;
317
+ }
318
+
319
+ return current;
320
+ }
321
+
322
+ function stricterUpperBoundary(
323
+ current: NumericBoundary | null,
324
+ candidate: NumericBoundary,
325
+ ): NumericBoundary {
326
+ if (
327
+ !current ||
328
+ candidate.value < current.value ||
329
+ (candidate.value === current.value && !candidate.inclusive)
330
+ ) {
331
+ return candidate;
332
+ }
333
+
334
+ return current;
335
+ }
336
+
337
+ function getNumericInterval(
338
+ predicate: ToolcraftControlPredicateSchema,
339
+ ): NumericInterval | null {
340
+ let lower: NumericBoundary | null = null;
341
+ let upper: NumericBoundary | null = null;
342
+
343
+ if (predicate.greaterThan !== undefined) {
344
+ lower = stricterLowerBoundary(lower, {
345
+ inclusive: false,
346
+ value: predicate.greaterThan,
347
+ });
348
+ }
349
+ if (predicate.greaterThanOrEqual !== undefined) {
350
+ lower = stricterLowerBoundary(lower, {
351
+ inclusive: true,
352
+ value: predicate.greaterThanOrEqual,
353
+ });
354
+ }
355
+ if (predicate.lessThan !== undefined) {
356
+ upper = stricterUpperBoundary(upper, {
357
+ inclusive: false,
358
+ value: predicate.lessThan,
359
+ });
360
+ }
361
+ if (predicate.lessThanOrEqual !== undefined) {
362
+ upper = stricterUpperBoundary(upper, {
363
+ inclusive: true,
364
+ value: predicate.lessThanOrEqual,
365
+ });
366
+ }
367
+
368
+ return lower || upper ? { lower, upper } : null;
369
+ }
370
+
371
+ function intervalEndsBefore(
372
+ upper: NumericBoundary | null,
373
+ lower: NumericBoundary | null,
374
+ ): boolean {
375
+ if (!upper || !lower) {
376
+ return false;
377
+ }
378
+
379
+ return (
380
+ upper.value < lower.value ||
381
+ (upper.value === lower.value && (!upper.inclusive || !lower.inclusive))
382
+ );
383
+ }
384
+
385
+ export function areToolcraftPredicatesProvablyExclusive(
386
+ left: ToolcraftControlPredicateSchema,
387
+ right: ToolcraftControlPredicateSchema,
388
+ ): boolean {
389
+ if (left.target !== right.target) {
390
+ return false;
391
+ }
392
+
393
+ const leftValues = getFinitePredicateValues(left);
394
+ if (
395
+ leftValues &&
396
+ leftValues.every(
397
+ (value) => !doesToolcraftPredicateMatchValue(right, value),
398
+ )
399
+ ) {
400
+ return true;
401
+ }
402
+
403
+ const rightValues = getFinitePredicateValues(right);
404
+ if (
405
+ rightValues &&
406
+ rightValues.every(
407
+ (value) => !doesToolcraftPredicateMatchValue(left, value),
408
+ )
409
+ ) {
410
+ return true;
411
+ }
412
+
413
+ const leftInterval = getNumericInterval(left);
414
+ const rightInterval = getNumericInterval(right);
415
+
416
+ return Boolean(
417
+ leftInterval &&
418
+ rightInterval &&
419
+ (intervalEndsBefore(leftInterval.upper, rightInterval.lower) ||
420
+ intervalEndsBefore(rightInterval.upper, leftInterval.lower)),
421
+ );
422
+ }
423
+
424
+ export function areToolcraftPredicateSetsProvablyExclusive(
425
+ left: readonly ToolcraftControlPredicateSchema[],
426
+ right: readonly ToolcraftControlPredicateSchema[],
427
+ ): boolean {
428
+ return left.some((leftPredicate) =>
429
+ right.some((rightPredicate) =>
430
+ areToolcraftPredicatesProvablyExclusive(leftPredicate, rightPredicate),
431
+ ),
432
+ );
433
+ }