@pixel-point/toolcraft 0.0.8 → 0.0.9

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 (44) hide show
  1. package/package.json +1 -1
  2. package/src/generate.mjs +34 -5
  3. package/src/generate.test.mjs +12 -0
  4. package/src/package-json.mjs +15 -0
  5. package/src/package-json.test.mjs +14 -1
  6. package/templates/runtime/contracts/component-contracts.test.ts +104 -14
  7. package/templates/runtime/contracts/component-contracts.ts +54 -22
  8. package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
  9. package/templates/runtime/contracts/decision-contracts.ts +3 -3
  10. package/templates/runtime/react/controls-panel.test.tsx +374 -13
  11. package/templates/runtime/react/controls-panel.tsx +65 -4
  12. package/templates/runtime/schema/define-toolcraft.test.ts +45 -1
  13. package/templates/runtime/schema/define-toolcraft.ts +25 -1
  14. package/templates/runtime/schema/types.ts +3 -0
  15. package/templates/runtime/testing/performance.test.ts +134 -0
  16. package/templates/runtime/testing/performance.ts +34 -4
  17. package/templates/starter/AGENTS.md +4 -4
  18. package/templates/starter/docs/toolcraft/README.md +1 -1
  19. package/templates/starter/docs/toolcraft/acceptance-testing.md +5 -3
  20. package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -5
  21. package/templates/starter/docs/toolcraft/component-rules.md +32 -11
  22. package/templates/starter/docs/toolcraft/performance.md +7 -1
  23. package/templates/starter/docs/toolcraft/schema-reference.md +43 -14
  24. package/templates/starter/gitignore +1 -0
  25. package/templates/starter/package.json +2 -0
  26. package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
  27. package/templates/starter/scripts/toolcraft-port.mjs +102 -0
  28. package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
  29. package/templates/starter/src/app/starter-acceptance.test.ts +739 -66
  30. package/templates/starter/src/app/starter-acceptance.ts +471 -12
  31. package/templates/ui/components/control-layout/index.tsx +8 -3
  32. package/templates/ui/components/controls/actions/actions-control.tsx +11 -3
  33. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  34. package/templates/ui/components/controls/color/index.ts +4 -1
  35. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  36. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  37. package/templates/ui/components/controls/index.ts +2 -0
  38. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  39. package/templates/ui/components/controls/select/select-control.tsx +9 -4
  40. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  41. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  42. package/templates/ui/components/controls/vector/index.ts +1 -0
  43. package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
  44. package/templates/ui/components/panel/panel-section.tsx +29 -5
@@ -13,6 +13,7 @@ import {
13
13
  getToolcraftControlOrderTargets,
14
14
  type ToolcraftComponentAcceptance,
15
15
  starterAcceptance,
16
+ starterControlSectionInventory,
16
17
  starterProductReadiness,
17
18
  starterTransferMode,
18
19
  validateToolcraftAcceptanceCoverage,
@@ -806,6 +807,26 @@ describe("Toolcraft template app acceptance coverage", () => {
806
807
  expect(validateToolcraftAcceptanceCoverage(starterSchema, starterAcceptance)).toEqual([]);
807
808
  });
808
809
 
810
+ it("requires generated product apps to publish a control section inventory", () => {
811
+ if (!schemaHasProductSurface()) {
812
+ expect(starterControlSectionInventory).toEqual([]);
813
+ return;
814
+ }
815
+
816
+ expect(
817
+ starterControlSectionInventory.length,
818
+ "Product apps must export starterControlSectionInventory so section grouping decisions are machine-checkable.",
819
+ ).toBeGreaterThan(0);
820
+ expect(
821
+ validateToolcraftAcceptanceCoverage(
822
+ starterSchema,
823
+ starterAcceptance,
824
+ starterTransferMode,
825
+ starterControlSectionInventory,
826
+ ),
827
+ ).toEqual([]);
828
+ });
829
+
809
830
  it("requires fileDrop acceptance to prove upload, clear, and reset lifecycle", () => {
810
831
  const fileDropSchema = defineToolcraft({
811
832
  canvas: { enabled: true },
@@ -994,7 +1015,7 @@ describe("Toolcraft template app acceptance coverage", () => {
994
1015
 
995
1016
  expect(validateToolcraftAcceptanceCoverage(fixedOutputSchema, starterAcceptance)).toEqual(
996
1017
  expect.arrayContaining([
997
- 'canvas.sizing mode "fixed-output" requires a runtime acceptance entry with canvasSizingCoverage "fixed-output-size" explaining why width and height are intentionally non-editable. A user-provided base/default size should normally use "editable-output".',
1018
+ 'canvas.sizing mode "fixed-output" requires a runtime acceptance entry with canvasSizingCoverage "fixed-output-size" explaining why width and height are intentionally non-editable. Product/output apps must use "editable-output"; user-provided, reference, fixed-format, or base/default sizes belong in canvas.size as editable initial values.',
998
1019
  ]),
999
1020
  );
1000
1021
  });
@@ -1047,7 +1068,118 @@ describe("Toolcraft template app acceptance coverage", () => {
1047
1068
  ]),
1048
1069
  ).toEqual(
1049
1070
  expect.arrayContaining([
1050
- 'canvas.sizing canvasSizingCoverage "fixed-output-size" must explain why the product output dimensions are intentionally fixed, not merely initialized from a default size.',
1071
+ 'canvas.sizing canvasSizingCoverage "fixed-output-size" must explain why the output dimensions are intentionally fixed for a non-product/internal fixture, not merely initialized from a default size.',
1072
+ ]),
1073
+ );
1074
+ });
1075
+
1076
+ it("rejects fixed canvas sizing justified only by a missing reference size editor", () => {
1077
+ const fixedOutputSchema = defineToolcraft({
1078
+ canvas: {
1079
+ enabled: true,
1080
+ size: { height: 1080, unit: "px", width: 1920 },
1081
+ sizing: { mode: "fixed-output" },
1082
+ },
1083
+ panels: {
1084
+ controls: {
1085
+ sections: [
1086
+ {
1087
+ controls: {
1088
+ prompt: {
1089
+ defaultValue: "Describe the effect",
1090
+ label: "Prompt",
1091
+ orderRole: "input",
1092
+ target: "generation.prompt",
1093
+ type: "text",
1094
+ },
1095
+ },
1096
+ title: "Generation",
1097
+ },
1098
+ ],
1099
+ title: "Controls",
1100
+ },
1101
+ },
1102
+ });
1103
+
1104
+ expect(
1105
+ validateToolcraftAcceptanceCoverage(fixedOutputSchema, [
1106
+ ...starterAcceptance,
1107
+ {
1108
+ automated: true,
1109
+ automatedTestName: "canvas output remains fixed",
1110
+ browser: true,
1111
+ browserTestName: "browser: canvas output remains fixed",
1112
+ canvasSizingCoverage: "fixed-output-size",
1113
+ componentType: "fixed-output canvas",
1114
+ evidence: "product-output",
1115
+ expectedObservable:
1116
+ "The WebGL output remains fixed at 1920x1080 because the reference app has no user-facing output size editor.",
1117
+ fixture: "default canvas fixture",
1118
+ id: "canvas.sizing",
1119
+ kind: "runtime",
1120
+ userAction: "Open the app.",
1121
+ },
1122
+ ]),
1123
+ ).toEqual(
1124
+ expect.arrayContaining([
1125
+ expect.stringContaining("cannot be justified by the reference or previous app lacking a size editor"),
1126
+ ]),
1127
+ );
1128
+ });
1129
+
1130
+ it("rejects fixed canvas sizing for exportable product apps even when reference dimensions are fixed", () => {
1131
+ const fixedExportSchema = defineToolcraft({
1132
+ canvas: {
1133
+ enabled: true,
1134
+ size: { height: 900, unit: "px", width: 1440 },
1135
+ sizing: { mode: "fixed-output" },
1136
+ },
1137
+ panels: {
1138
+ controls: {
1139
+ sections: [
1140
+ {
1141
+ controls: {
1142
+ outputActions: {
1143
+ actions: [
1144
+ {
1145
+ icon: "export",
1146
+ label: "Export PNG",
1147
+ value: "export.png",
1148
+ },
1149
+ ],
1150
+ target: "actions.output",
1151
+ type: "panelActions",
1152
+ },
1153
+ },
1154
+ },
1155
+ ],
1156
+ title: "Controls",
1157
+ },
1158
+ },
1159
+ });
1160
+
1161
+ expect(
1162
+ validateToolcraftAcceptanceCoverage(fixedExportSchema, [
1163
+ ...starterAcceptance,
1164
+ {
1165
+ automated: true,
1166
+ automatedTestName: "fixed reference size is preserved",
1167
+ browser: true,
1168
+ browserTestName: "browser: fixed reference size is preserved",
1169
+ canvasSizingCoverage: "fixed-output-size",
1170
+ componentType: "fixed-output canvas",
1171
+ evidence: "product-output",
1172
+ expectedObservable:
1173
+ "The exported shader canvas remains reference-defined and fixed at 1440x900.",
1174
+ fixture: "shader reference fixture",
1175
+ id: "canvas.sizing",
1176
+ kind: "runtime",
1177
+ userAction: "Open the app.",
1178
+ },
1179
+ ]),
1180
+ ).toEqual(
1181
+ expect.arrayContaining([
1182
+ 'Product/output apps with export actions must use canvas.sizing mode "editable-output" so Aspect ratio, Canvas width, and Canvas height are always available. Put reference, fixed-format, or user-requested dimensions in canvas.size as the initial value instead of hiding size controls with "fixed-output".',
1051
1183
  ]),
1052
1184
  );
1053
1185
  });
@@ -2109,7 +2241,6 @@ describe("Toolcraft template app acceptance coverage", () => {
2109
2241
  step: 0.1,
2110
2242
  target: "field.speed",
2111
2243
  type: "slider",
2112
- unit: "x",
2113
2244
  },
2114
2245
  },
2115
2246
  layoutGroups: [
@@ -2167,6 +2298,57 @@ describe("Toolcraft template app acceptance coverage", () => {
2167
2298
  );
2168
2299
  });
2169
2300
 
2301
+ it("rejects x units on slider value labels", () => {
2302
+ const schema = defineToolcraft({
2303
+ canvas: { enabled: true },
2304
+ panels: {
2305
+ controls: {
2306
+ sections: [
2307
+ {
2308
+ controls: {
2309
+ strength: {
2310
+ defaultValue: 0.4,
2311
+ label: "Strength",
2312
+ max: 1,
2313
+ min: 0,
2314
+ step: 0.01,
2315
+ target: "glass.strength",
2316
+ type: "slider",
2317
+ unit: "x",
2318
+ },
2319
+ },
2320
+ title: "Glass",
2321
+ },
2322
+ ],
2323
+ title: "Controls",
2324
+ },
2325
+ },
2326
+ });
2327
+
2328
+ expect(
2329
+ validateToolcraftAcceptanceCoverage(schema, [
2330
+ {
2331
+ automated: true,
2332
+ automatedTestName: "strength changes glass output",
2333
+ browser: true,
2334
+ browserTestName: "browser: strength slider changes glass output",
2335
+ componentType: "slider",
2336
+ evidence: "rendered-pixels",
2337
+ expectedObservable: "Changing Strength changes the rendered glass strength.",
2338
+ fixture: "glass strength fixture",
2339
+ id: "glass.strength",
2340
+ kind: "control",
2341
+ target: "glass.strength",
2342
+ userAction: "Drag the Strength slider.",
2343
+ },
2344
+ ]),
2345
+ ).toEqual(
2346
+ expect.arrayContaining([
2347
+ 'Glass / strength (glass.strength) uses unit "x", but Toolcraft slider values do not use x suffixes. Omit unit for scale, multiplier, intensity, opacity, strength, depth, and shader amount values unless a real measurement unit applies.',
2348
+ ]),
2349
+ );
2350
+ });
2351
+
2170
2352
  it("accepts compound controls only when every semantic value part is declared", () => {
2171
2353
  const gradientSchema = defineToolcraft({
2172
2354
  canvas: { enabled: true },
@@ -3872,6 +4054,78 @@ describe("Toolcraft template app acceptance coverage", () => {
3872
4054
  );
3873
4055
  });
3874
4056
 
4057
+ it("rejects single Actions controls that duplicate their only button label", () => {
4058
+ const schemaWithDuplicateActionLabel = defineToolcraft({
4059
+ canvas: { enabled: true },
4060
+ panels: {
4061
+ controls: {
4062
+ sections: [
4063
+ {
4064
+ controls: {
4065
+ wash: {
4066
+ actions: [{ label: "Wash", value: "wash" }],
4067
+ defaultValue: null,
4068
+ label: "Wash",
4069
+ target: "flow.washSignal",
4070
+ type: "actions",
4071
+ },
4072
+ },
4073
+ title: "Flow",
4074
+ },
4075
+ ],
4076
+ title: "Controls",
4077
+ },
4078
+ },
4079
+ });
4080
+
4081
+ expect(
4082
+ validateToolcraftAcceptanceCoverage(schemaWithDuplicateActionLabel, [
4083
+ makeControlAcceptance("flow.washSignal", "actions"),
4084
+ ]),
4085
+ ).toEqual(
4086
+ expect.arrayContaining([
4087
+ expect.stringContaining(
4088
+ 'flow.washSignal) single Actions control label "Wash" duplicates its only button label "Wash". Keep the button as the command and use a short context label such as "Ink wash", "Palette action", or "Current layer".',
4089
+ ),
4090
+ ]),
4091
+ );
4092
+ });
4093
+
4094
+ it("allows single Actions controls with a concise context label", () => {
4095
+ const schemaWithContextActionLabel = defineToolcraft({
4096
+ canvas: { enabled: true },
4097
+ panels: {
4098
+ controls: {
4099
+ sections: [
4100
+ {
4101
+ controls: {
4102
+ wash: {
4103
+ actions: [{ label: "Wash", value: "wash" }],
4104
+ defaultValue: null,
4105
+ label: "Ink wash",
4106
+ target: "flow.washSignal",
4107
+ type: "actions",
4108
+ },
4109
+ },
4110
+ title: "Flow",
4111
+ },
4112
+ ],
4113
+ title: "Controls",
4114
+ },
4115
+ },
4116
+ });
4117
+
4118
+ const errors = validateToolcraftAcceptanceCoverage(schemaWithContextActionLabel, [
4119
+ makeControlAcceptance("flow.washSignal", "actions"),
4120
+ ]);
4121
+
4122
+ expect(errors).not.toEqual(
4123
+ expect.arrayContaining([
4124
+ expect.stringContaining("single Actions control label"),
4125
+ ]),
4126
+ );
4127
+ });
4128
+
3875
4129
  it("allows short Include toggle plus an unlabeled background color in the required row", () => {
3876
4130
  const schema = defineToolcraft({
3877
4131
  canvas: { enabled: true },
@@ -3941,7 +4195,7 @@ describe("Toolcraft template app acceptance coverage", () => {
3941
4195
  },
3942
4196
  duration: {
3943
4197
  defaultValue: "8",
3944
- label: "Duration",
4198
+ label: false,
3945
4199
  target: "animation.duration",
3946
4200
  type: "text",
3947
4201
  },
@@ -3971,7 +4225,7 @@ describe("Toolcraft template app acceptance coverage", () => {
3971
4225
  );
3972
4226
  });
3973
4227
 
3974
- it("rejects long visible toggle labels beside one related parameter", () => {
4228
+ it("rejects CodeTextarea for short single-line text content", () => {
3975
4229
  const schema = defineToolcraft({
3976
4230
  canvas: { enabled: true },
3977
4231
  panels: {
@@ -3979,27 +4233,14 @@ describe("Toolcraft template app acceptance coverage", () => {
3979
4233
  sections: [
3980
4234
  {
3981
4235
  controls: {
3982
- transparentBackground: {
3983
- defaultValue: true,
3984
- label: "Transparent background",
3985
- target: "export.transparentBackground",
3986
- type: "switch",
3987
- },
3988
- background: {
3989
- defaultValue: "#0F0F0F",
3990
- label: "Background",
3991
- target: "appearance.background",
3992
- type: "color",
4236
+ buttonText: {
4237
+ defaultValue: "Glass",
4238
+ label: "Text",
4239
+ target: "button.text",
4240
+ type: "code",
3993
4241
  },
3994
4242
  },
3995
- layoutGroups: [
3996
- {
3997
- columns: 2,
3998
- controls: ["transparentBackground", "background"],
3999
- layout: "inline",
4000
- },
4001
- ],
4002
- title: "Background",
4243
+ title: "Button",
4003
4244
  },
4004
4245
  ],
4005
4246
  title: "Controls",
@@ -4009,19 +4250,16 @@ describe("Toolcraft template app acceptance coverage", () => {
4009
4250
 
4010
4251
  expect(
4011
4252
  validateToolcraftAcceptanceCoverage(schema, [
4012
- makeControlAcceptance("export.transparentBackground", "switch"),
4013
- makeControlAcceptance("appearance.background", "color"),
4253
+ makeControlAcceptance("button.text", "code"),
4014
4254
  ]),
4015
4255
  ).toEqual(
4016
4256
  expect.arrayContaining([
4017
- expect.stringContaining(
4018
- 'Background layoutGroups inline row "transparentBackground, background" includes toggle label transparentBackground "Transparent background" that is too long for a compact toggle-plus-parameter row.',
4019
- ),
4257
+ expect.stringContaining("uses CodeTextarea for short single-line text"),
4020
4258
  ]),
4021
4259
  );
4022
4260
  });
4023
4261
 
4024
- it("allows inline switch pairs when both labels are compact", () => {
4262
+ it("allows CodeTextarea when a short default documents long multiline intent", () => {
4025
4263
  const schema = defineToolcraft({
4026
4264
  canvas: { enabled: true },
4027
4265
  panels: {
@@ -4029,27 +4267,16 @@ describe("Toolcraft template app acceptance coverage", () => {
4029
4267
  sections: [
4030
4268
  {
4031
4269
  controls: {
4032
- glow: {
4033
- defaultValue: true,
4034
- label: "Glow",
4035
- target: "style.glow",
4036
- type: "switch",
4037
- },
4038
- loop: {
4039
- defaultValue: false,
4040
- label: "Loop",
4041
- target: "animation.loop",
4042
- type: "switch",
4270
+ prompt: {
4271
+ defaultValue: "Describe the scene",
4272
+ description:
4273
+ "Long multiline prompt content for generated output.",
4274
+ label: "Prompt",
4275
+ target: "generation.prompt",
4276
+ type: "code",
4043
4277
  },
4044
4278
  },
4045
- layoutGroups: [
4046
- {
4047
- columns: 2,
4048
- controls: ["glow", "loop"],
4049
- layout: "inline",
4050
- },
4051
- ],
4052
- title: "Style",
4279
+ title: "Generation",
4053
4280
  },
4054
4281
  ],
4055
4282
  title: "Controls",
@@ -4057,10 +4284,211 @@ describe("Toolcraft template app acceptance coverage", () => {
4057
4284
  },
4058
4285
  });
4059
4286
 
4060
- const errors = validateToolcraftAcceptanceCoverage(schema, [
4061
- makeControlAcceptance("style.glow", "switch"),
4062
- makeControlAcceptance("animation.loop", "switch"),
4063
- ]);
4287
+ expect(
4288
+ validateToolcraftAcceptanceCoverage(schema, [
4289
+ makeControlAcceptance("generation.prompt", "code"),
4290
+ ]),
4291
+ ).not.toEqual(
4292
+ expect.arrayContaining([
4293
+ expect.stringContaining("uses CodeTextarea for short single-line text"),
4294
+ ]),
4295
+ );
4296
+ });
4297
+
4298
+ it("rejects visible parameter labels beside one related toggle", () => {
4299
+ const schema = defineToolcraft({
4300
+ canvas: { enabled: true },
4301
+ panels: {
4302
+ controls: {
4303
+ sections: [
4304
+ {
4305
+ controls: {
4306
+ loop: {
4307
+ defaultValue: true,
4308
+ label: "Loop",
4309
+ target: "animation.loop",
4310
+ type: "switch",
4311
+ },
4312
+ duration: {
4313
+ defaultValue: "8",
4314
+ label: "Duration",
4315
+ target: "animation.duration",
4316
+ type: "text",
4317
+ },
4318
+ },
4319
+ layoutGroups: [
4320
+ {
4321
+ columns: 2,
4322
+ controls: ["loop", "duration"],
4323
+ layout: "inline",
4324
+ },
4325
+ ],
4326
+ title: "Playback",
4327
+ },
4328
+ ],
4329
+ title: "Controls",
4330
+ },
4331
+ },
4332
+ });
4333
+
4334
+ expect(
4335
+ validateToolcraftAcceptanceCoverage(schema, [
4336
+ makeControlAcceptance("animation.loop", "switch"),
4337
+ makeControlAcceptance("animation.duration", "text"),
4338
+ ]),
4339
+ ).toEqual(
4340
+ expect.arrayContaining([
4341
+ expect.stringContaining(
4342
+ 'Playback layoutGroups inline row "loop, duration" pairs a toggle with parameter labels duration "Duration". In toggle-plus-parameter rows, the non-toggle parameter must use label false; if that label is needed, stack the controls instead.',
4343
+ ),
4344
+ ]),
4345
+ );
4346
+ });
4347
+
4348
+ it("rejects segmented controls in inline half-width layout rows", () => {
4349
+ const schema = defineToolcraft({
4350
+ canvas: { enabled: true },
4351
+ panels: {
4352
+ controls: {
4353
+ sections: [
4354
+ {
4355
+ controls: {
4356
+ includeText: {
4357
+ defaultValue: true,
4358
+ label: "Include",
4359
+ target: "text.enabled",
4360
+ type: "switch",
4361
+ },
4362
+ dragTarget: {
4363
+ defaultValue: "glass",
4364
+ label: "Drag",
4365
+ options: [
4366
+ { label: "Glass", value: "glass" },
4367
+ { label: "Text", value: "text" },
4368
+ ],
4369
+ target: "text.dragTarget",
4370
+ type: "segmented",
4371
+ },
4372
+ },
4373
+ layoutGroups: [
4374
+ {
4375
+ columns: 2,
4376
+ controls: ["includeText", "dragTarget"],
4377
+ layout: "inline",
4378
+ },
4379
+ ],
4380
+ title: "Glass Text",
4381
+ },
4382
+ ],
4383
+ title: "Controls",
4384
+ },
4385
+ },
4386
+ });
4387
+
4388
+ expect(
4389
+ validateToolcraftAcceptanceCoverage(schema, [
4390
+ makeControlAcceptance("text.enabled", "switch"),
4391
+ makeControlAcceptance("text.dragTarget", "segmented"),
4392
+ ]),
4393
+ ).toEqual(
4394
+ expect.arrayContaining([
4395
+ expect.stringContaining(
4396
+ 'Glass Text layoutGroups inline row "includeText, dragTarget" includes segmented control dragTarget. Segmented is full-width and must not share a two-column or half-width row; use Select when a finite choice must fit beside another control.',
4397
+ ),
4398
+ ]),
4399
+ );
4400
+ });
4401
+
4402
+ it("rejects long visible toggle labels beside one related parameter", () => {
4403
+ const schema = defineToolcraft({
4404
+ canvas: { enabled: true },
4405
+ panels: {
4406
+ controls: {
4407
+ sections: [
4408
+ {
4409
+ controls: {
4410
+ transparentBackground: {
4411
+ defaultValue: true,
4412
+ label: "Transparent background",
4413
+ target: "export.transparentBackground",
4414
+ type: "switch",
4415
+ },
4416
+ background: {
4417
+ defaultValue: "#0F0F0F",
4418
+ label: "Background",
4419
+ target: "appearance.background",
4420
+ type: "color",
4421
+ },
4422
+ },
4423
+ layoutGroups: [
4424
+ {
4425
+ columns: 2,
4426
+ controls: ["transparentBackground", "background"],
4427
+ layout: "inline",
4428
+ },
4429
+ ],
4430
+ title: "Background",
4431
+ },
4432
+ ],
4433
+ title: "Controls",
4434
+ },
4435
+ },
4436
+ });
4437
+
4438
+ expect(
4439
+ validateToolcraftAcceptanceCoverage(schema, [
4440
+ makeControlAcceptance("export.transparentBackground", "switch"),
4441
+ makeControlAcceptance("appearance.background", "color"),
4442
+ ]),
4443
+ ).toEqual(
4444
+ expect.arrayContaining([
4445
+ expect.stringContaining(
4446
+ 'Background layoutGroups inline row "transparentBackground, background" includes toggle label transparentBackground "Transparent background" that is too long for a compact toggle-plus-parameter row.',
4447
+ ),
4448
+ ]),
4449
+ );
4450
+ });
4451
+
4452
+ it("allows inline switch pairs when both labels are compact", () => {
4453
+ const schema = defineToolcraft({
4454
+ canvas: { enabled: true },
4455
+ panels: {
4456
+ controls: {
4457
+ sections: [
4458
+ {
4459
+ controls: {
4460
+ glow: {
4461
+ defaultValue: true,
4462
+ label: "Glow",
4463
+ target: "style.glow",
4464
+ type: "switch",
4465
+ },
4466
+ loop: {
4467
+ defaultValue: false,
4468
+ label: "Loop",
4469
+ target: "animation.loop",
4470
+ type: "switch",
4471
+ },
4472
+ },
4473
+ layoutGroups: [
4474
+ {
4475
+ columns: 2,
4476
+ controls: ["glow", "loop"],
4477
+ layout: "inline",
4478
+ },
4479
+ ],
4480
+ title: "Style",
4481
+ },
4482
+ ],
4483
+ title: "Controls",
4484
+ },
4485
+ },
4486
+ });
4487
+
4488
+ const errors = validateToolcraftAcceptanceCoverage(schema, [
4489
+ makeControlAcceptance("style.glow", "switch"),
4490
+ makeControlAcceptance("animation.loop", "switch"),
4491
+ ]);
4064
4492
 
4065
4493
  expect(errors).not.toEqual(
4066
4494
  expect.arrayContaining([expect.stringContaining("two-column toggle row")]),
@@ -5252,7 +5680,6 @@ describe("Toolcraft template app acceptance coverage", () => {
5252
5680
  step: 0.1,
5253
5681
  target: "animation.speed",
5254
5682
  type: "slider",
5255
- unit: "x",
5256
5683
  variant: "continuous",
5257
5684
  },
5258
5685
  depth: {
@@ -6145,7 +6572,7 @@ describe("Toolcraft template app acceptance coverage", () => {
6145
6572
  ]),
6146
6573
  ).toEqual(
6147
6574
  expect.arrayContaining([
6148
- 'Controls for product entity "squares.right" are split across sections: Square 1 (Right), Appearance. Keep controls for the same product entity in one semantic section unless the spec names a real workflow split.',
6575
+ 'Controls for product entity "squares.right" are split across sections: Square 1 (Right), Appearance. Keep controls for the same product entity in one semantic section unless the Control Section Inventory declares workflowStage and splitReason for every split section.',
6149
6576
  ]),
6150
6577
  );
6151
6578
  });
@@ -6201,6 +6628,252 @@ describe("Toolcraft template app acceptance coverage", () => {
6201
6628
  ).toEqual([]);
6202
6629
  });
6203
6630
 
6631
+ it("rejects stale control section inventory entries", () => {
6632
+ const schemaWithInventoryMismatch = defineToolcraft({
6633
+ canvas: { enabled: true },
6634
+ panels: {
6635
+ controls: {
6636
+ sections: [
6637
+ {
6638
+ controls: {
6639
+ kind: {
6640
+ defaultValue: "soft",
6641
+ label: "Kind",
6642
+ options: [
6643
+ { label: "Soft", value: "soft" },
6644
+ { label: "Sharp", value: "sharp" },
6645
+ ],
6646
+ orderRole: "mode",
6647
+ target: "shape.kind",
6648
+ type: "select",
6649
+ },
6650
+ count: {
6651
+ defaultValue: 12,
6652
+ label: "Count",
6653
+ max: 40,
6654
+ min: 1,
6655
+ orderRole: "detail",
6656
+ target: "shape.count",
6657
+ type: "slider",
6658
+ variant: "continuous",
6659
+ },
6660
+ },
6661
+ title: "Shape",
6662
+ },
6663
+ ],
6664
+ title: "Controls",
6665
+ },
6666
+ },
6667
+ });
6668
+
6669
+ expect(
6670
+ validateToolcraftAcceptanceCoverage(
6671
+ schemaWithInventoryMismatch,
6672
+ [
6673
+ makeControlAcceptance("shape.kind", "select"),
6674
+ makeControlAcceptance("shape.count", "slider"),
6675
+ ],
6676
+ starterTransferMode,
6677
+ [
6678
+ {
6679
+ entity: "Shape",
6680
+ groupingReason: "Shape setup",
6681
+ targets: ["shape.kind"],
6682
+ title: "Shape",
6683
+ },
6684
+ ],
6685
+ ),
6686
+ ).toEqual(
6687
+ expect.arrayContaining([
6688
+ 'Control Section Inventory entry "Shape" must include a concrete groupingReason explaining why these controls belong together.',
6689
+ 'Control Section Inventory entry "Shape" is missing rendered target "shape.count". The inventory must cover every product control in the section.',
6690
+ ]),
6691
+ );
6692
+ });
6693
+
6694
+ it("allows explicit workflow split evidence for one target entity", () => {
6695
+ const schemaWithWorkflowSplit = defineToolcraft({
6696
+ canvas: { enabled: true },
6697
+ panels: {
6698
+ controls: {
6699
+ sections: [
6700
+ {
6701
+ controls: {
6702
+ size: {
6703
+ defaultValue: 64,
6704
+ label: "Size",
6705
+ max: 128,
6706
+ min: 16,
6707
+ orderRole: "primary",
6708
+ target: "object.shape.size",
6709
+ type: "slider",
6710
+ unit: "px",
6711
+ variant: "continuous",
6712
+ },
6713
+ },
6714
+ title: "Shape Structure",
6715
+ },
6716
+ {
6717
+ controls: {
6718
+ count: {
6719
+ defaultValue: 12,
6720
+ label: "Count",
6721
+ max: 40,
6722
+ min: 1,
6723
+ orderRole: "detail",
6724
+ target: "object.shape.count",
6725
+ type: "slider",
6726
+ variant: "continuous",
6727
+ },
6728
+ },
6729
+ title: "Shape Density",
6730
+ },
6731
+ ],
6732
+ title: "Controls",
6733
+ },
6734
+ },
6735
+ });
6736
+
6737
+ expect(
6738
+ validateToolcraftAcceptanceCoverage(
6739
+ schemaWithWorkflowSplit,
6740
+ [
6741
+ makeControlAcceptance("object.shape.size", "slider"),
6742
+ makeControlAcceptance("object.shape.count", "slider"),
6743
+ ],
6744
+ starterTransferMode,
6745
+ [
6746
+ {
6747
+ entity: "Object shape",
6748
+ groupingReason: "Structure controls tune the physical footprint of the object.",
6749
+ splitReason:
6750
+ "Structure and density are separate workflow stages in this editor.",
6751
+ targets: ["object.shape.size"],
6752
+ title: "Shape Structure",
6753
+ workflowStage: "structure",
6754
+ },
6755
+ {
6756
+ entity: "Object shape",
6757
+ groupingReason: "Density controls tune how many objects appear after structure is set.",
6758
+ splitReason:
6759
+ "Density is separated from structure because users set count after the shape footprint.",
6760
+ targets: ["object.shape.count"],
6761
+ title: "Shape Density",
6762
+ workflowStage: "density",
6763
+ },
6764
+ ],
6765
+ ),
6766
+ ).toEqual([]);
6767
+ });
6768
+
6769
+ it("rejects mode-gated controls split away from their selector section", () => {
6770
+ const schemaWithSplitModeBranch = defineToolcraft({
6771
+ canvas: { enabled: true },
6772
+ panels: {
6773
+ controls: {
6774
+ sections: [
6775
+ {
6776
+ controls: {
6777
+ sourceMode: {
6778
+ defaultValue: "preset",
6779
+ label: "Source",
6780
+ options: [
6781
+ { label: "Preset", value: "preset" },
6782
+ { label: "Image", value: "image" },
6783
+ ],
6784
+ orderRole: "mode",
6785
+ target: "source.mode",
6786
+ type: "segmented",
6787
+ },
6788
+ },
6789
+ title: "Source",
6790
+ },
6791
+ {
6792
+ controls: {
6793
+ sourceUpload: {
6794
+ accept: "image/*",
6795
+ assetKind: "image",
6796
+ defaultValue: null,
6797
+ label: "Image",
6798
+ orderRole: "input",
6799
+ target: "source.upload",
6800
+ type: "fileDrop",
6801
+ visibleWhen: { equals: "image", target: "source.mode" },
6802
+ },
6803
+ },
6804
+ title: "Image",
6805
+ },
6806
+ ],
6807
+ title: "Controls",
6808
+ },
6809
+ },
6810
+ });
6811
+
6812
+ expect(
6813
+ validateToolcraftAcceptanceCoverage(schemaWithSplitModeBranch, [
6814
+ makeControlAcceptance("source.mode", "segmented"),
6815
+ makeControlAcceptance("source.upload", "fileDrop"),
6816
+ ]),
6817
+ ).toEqual(
6818
+ expect.arrayContaining([
6819
+ 'Image / sourceUpload is gated by visibleWhen target "source.mode" in Source, but it belongs to the same dependency group. Keep selectors and their dependent controls in one semantic section when they describe one product entity or branch; use visibleWhen/disabledWhen inside that section instead of splitting branch controls into their own section.',
6820
+ ]),
6821
+ );
6822
+ });
6823
+
6824
+ it("rejects selector branch sections even when targets do not share a prefix", () => {
6825
+ const schemaWithSplitOptionBranch = defineToolcraft({
6826
+ canvas: { enabled: true },
6827
+ panels: {
6828
+ controls: {
6829
+ sections: [
6830
+ {
6831
+ controls: {
6832
+ shapeKind: {
6833
+ defaultValue: "generated",
6834
+ label: "Shape",
6835
+ options: [
6836
+ { label: "Generated", value: "generated" },
6837
+ { label: "Library", value: "library" },
6838
+ ],
6839
+ orderRole: "mode",
6840
+ target: "shape.kind",
6841
+ type: "segmented",
6842
+ },
6843
+ },
6844
+ title: "Shape",
6845
+ },
6846
+ {
6847
+ controls: {
6848
+ libraryAsset: {
6849
+ defaultValue: null,
6850
+ label: "Library",
6851
+ orderRole: "input",
6852
+ target: "asset.upload",
6853
+ type: "fileDrop",
6854
+ visibleWhen: { equals: "library", target: "shape.kind" },
6855
+ },
6856
+ },
6857
+ title: "Library",
6858
+ },
6859
+ ],
6860
+ title: "Controls",
6861
+ },
6862
+ },
6863
+ });
6864
+
6865
+ expect(
6866
+ validateToolcraftAcceptanceCoverage(schemaWithSplitOptionBranch, [
6867
+ makeControlAcceptance("shape.kind", "segmented"),
6868
+ makeControlAcceptance("asset.upload", "fileDrop"),
6869
+ ]),
6870
+ ).toEqual(
6871
+ expect.arrayContaining([
6872
+ 'Library / libraryAsset is gated by visibleWhen target "shape.kind" in Shape, but it belongs to the same dependency group. Keep selectors and their dependent controls in one semantic section when they describe one product entity or branch; use visibleWhen/disabledWhen inside that section instead of splitting branch controls into their own section.',
6873
+ ]),
6874
+ );
6875
+ });
6876
+
6204
6877
  it("rejects splitting FontPicker-owned typography into sibling controls", () => {
6205
6878
  const schemaWithSplitTypographyBlock = defineToolcraft({
6206
6879
  canvas: { enabled: true },
@@ -6580,15 +7253,15 @@ describe("Toolcraft template app acceptance coverage", () => {
6580
7253
  type: "slider",
6581
7254
  variant: "continuous",
6582
7255
  },
6583
- blend: {
7256
+ mode: {
6584
7257
  defaultValue: "liquid",
6585
- label: "Blend",
7258
+ label: "Mode",
6586
7259
  options: [
6587
7260
  { label: "Silk", value: "silk" },
6588
7261
  { label: "Liquid", value: "liquid" },
6589
7262
  { label: "Crystal", value: "crystal" },
6590
7263
  ],
6591
- target: "shader.blend",
7264
+ target: "shader.mode",
6592
7265
  type: "segmented",
6593
7266
  },
6594
7267
  },
@@ -6618,23 +7291,23 @@ describe("Toolcraft template app acceptance coverage", () => {
6618
7291
  },
6619
7292
  {
6620
7293
  automated: true,
6621
- automatedTestName: "blend changes rendered output",
7294
+ automatedTestName: "mode changes rendered output",
6622
7295
  browser: true,
6623
- browserTestName: "browser: blend selector changes rendered output",
7296
+ browserTestName: "browser: mode selector changes rendered output",
6624
7297
  componentType: "segmented",
6625
7298
  evidence: "rendered-pixels",
6626
- expectedObservable: "Changing Blend switches shader pattern.",
6627
- fixture: "blend fixture",
6628
- id: "shader.blend",
7299
+ expectedObservable: "Changing Mode switches shader pattern.",
7300
+ fixture: "mode fixture",
7301
+ id: "shader.mode",
6629
7302
  kind: "control",
6630
7303
  optionCoverage: "each-visible-item",
6631
- target: "shader.blend",
6632
- userAction: "Select each Blend option.",
7304
+ target: "shader.mode",
7305
+ userAction: "Select each Mode option.",
6633
7306
  },
6634
7307
  ]),
6635
7308
  ).toEqual(
6636
7309
  expect.arrayContaining([
6637
- 'Volume / blend (shader.blend) has orderRole "mode" after depth (shader.depth) with orderRole "strength". Move mode/input/primary controls before dependent strength/detail/advanced controls or split them into an earlier section.',
7310
+ 'Volume / mode (shader.mode) has orderRole "mode" after depth (shader.depth) with orderRole "strength". Move mode/input/primary controls before dependent strength/detail/advanced controls or split them into an earlier section.',
6638
7311
  ]),
6639
7312
  );
6640
7313
  });