@pixel-point/toolcraft 0.0.7 → 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 (62) 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 +185 -23
  7. package/templates/runtime/contracts/component-contracts.ts +91 -36
  8. package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
  9. package/templates/runtime/contracts/decision-contracts.ts +4 -4
  10. package/templates/runtime/export/export.test.ts +31 -0
  11. package/templates/runtime/export/export.ts +41 -0
  12. package/templates/runtime/react/canvas-shell.test.tsx +77 -1
  13. package/templates/runtime/react/canvas-shell.tsx +178 -23
  14. package/templates/runtime/react/control-conditions.ts +166 -0
  15. package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
  16. package/templates/runtime/react/controls-panel.test.tsx +774 -17
  17. package/templates/runtime/react/controls-panel.tsx +155 -8
  18. package/templates/runtime/react/media-file.ts +19 -0
  19. package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
  20. package/templates/runtime/schema/define-toolcraft.ts +29 -3
  21. package/templates/runtime/schema/types.ts +7 -0
  22. package/templates/runtime/state/reducer.test.ts +304 -0
  23. package/templates/runtime/state/reducer.ts +148 -9
  24. package/templates/runtime/state/types.ts +10 -2
  25. package/templates/runtime/testing/performance.test.ts +1424 -56
  26. package/templates/runtime/testing/performance.ts +710 -43
  27. package/templates/starter/AGENTS.md +10 -9
  28. package/templates/starter/docs/toolcraft/README.md +1 -1
  29. package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
  30. package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
  31. package/templates/starter/docs/toolcraft/component-rules.md +46 -16
  32. package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
  33. package/templates/starter/docs/toolcraft/performance.md +54 -6
  34. package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
  35. package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
  36. package/templates/starter/docs/toolcraft/workflow.md +2 -2
  37. package/templates/starter/e2e/app-performance.spec.ts +136 -3
  38. package/templates/starter/e2e/performance-helpers.ts +197 -0
  39. package/templates/starter/gitignore +1 -0
  40. package/templates/starter/package.json +5 -0
  41. package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
  42. package/templates/starter/scripts/toolcraft-port.mjs +102 -0
  43. package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
  44. package/templates/starter/src/app/starter-acceptance.test.ts +1288 -105
  45. package/templates/starter/src/app/starter-acceptance.ts +740 -24
  46. package/templates/starter/src/app/starter-performance.test.ts +66 -5
  47. package/templates/ui/components/control-layout/index.tsx +8 -3
  48. package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
  49. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  50. package/templates/ui/components/controls/color/index.ts +4 -1
  51. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  52. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  53. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
  54. package/templates/ui/components/controls/file-drop/index.ts +1 -1
  55. package/templates/ui/components/controls/index.ts +3 -0
  56. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  57. package/templates/ui/components/controls/select/select-control.tsx +9 -4
  58. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  59. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  60. package/templates/ui/components/controls/vector/index.ts +1 -0
  61. package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
  62. package/templates/ui/components/panel/panel-section.tsx +82 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixel-point/toolcraft",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
6
  "bin": {
package/src/generate.mjs CHANGED
@@ -11,7 +11,11 @@ import {
11
11
  pathExists,
12
12
  removeDirectory,
13
13
  } from "./copy-recursive.mjs";
14
- import { createGeneratedPackageJson, createGeneratedTsConfig } from "./package-json.mjs";
14
+ import {
15
+ createGeneratedPackageJson,
16
+ createGeneratedTsConfig,
17
+ normalizeProjectTitle,
18
+ } from "./package-json.mjs";
15
19
  import { rewriteGeneratedText, rewriteTextFiles } from "./rewrite-imports.mjs";
16
20
 
17
21
  const PACKAGE_ROOT = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
@@ -21,6 +25,13 @@ function writeJson(filePath, value) {
21
25
  return fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
22
26
  }
23
27
 
28
+ function escapeHtmlText(value) {
29
+ return String(value)
30
+ .replaceAll("&", "&")
31
+ .replaceAll("<", "&lt;")
32
+ .replaceAll(">", "&gt;");
33
+ }
34
+
24
35
  async function readJson(filePath) {
25
36
  return JSON.parse(await fs.readFile(filePath, "utf8"));
26
37
  }
@@ -100,6 +111,21 @@ async function restoreGeneratedGitignore(targetDir) {
100
111
  await fs.rename(npmSafeGitignorePath, gitignorePath);
101
112
  }
102
113
 
114
+ async function writeGeneratedProjectTitle(targetDir, title) {
115
+ const indexPath = path.join(targetDir, "index.html");
116
+ const source = await fs.readFile(indexPath, "utf8");
117
+ const nextSource = source.replace(
118
+ /<title>.*?<\/title>/,
119
+ `<title>${escapeHtmlText(title)}</title>`,
120
+ );
121
+
122
+ if (nextSource === source) {
123
+ throw new Error(`Unable to update generated app title in ${indexPath}.`);
124
+ }
125
+
126
+ await fs.writeFile(indexPath, nextSource);
127
+ }
128
+
103
129
  async function removeToolcraftTestFiles(toolcraftRoot) {
104
130
  async function visit(currentDir) {
105
131
  const entries = await fs.readdir(currentDir, { withFileTypes: true });
@@ -190,6 +216,12 @@ export async function generateToolcraft(options = {}) {
190
216
  await assertDirectory(sourcePaths.uiSrc, "UI package source");
191
217
  await assertDirectory(sourcePaths.toolcraftSrc, "Toolcraft template runtime package source");
192
218
  const starterPackageJson = await readJson(path.join(sourcePaths.starterDir, "package.json"));
219
+ const packageJson = createGeneratedPackageJson({
220
+ name: options.name ?? path.basename(targetDir),
221
+ starterPackageJson,
222
+ });
223
+ const projectTitle = normalizeProjectTitle(packageJson.name);
224
+
193
225
  await ensureWritableTargetDirectory(targetDir, { force: options.force });
194
226
 
195
227
  await copyDirectory(sourcePaths.starterDir, targetDir);
@@ -206,11 +238,8 @@ export async function generateToolcraft(options = {}) {
206
238
  const changedFiles = await rewriteTextFiles(targetDir, (source) =>
207
239
  rewriteGeneratedAppText(source),
208
240
  );
241
+ await writeGeneratedProjectTitle(targetDir, projectTitle);
209
242
  await writeToolcraftIntegrityManifest(toolcraftRoot);
210
- const packageJson = createGeneratedPackageJson({
211
- name: options.name ?? path.basename(targetDir),
212
- starterPackageJson,
213
- });
214
243
 
215
244
  await writeJson(path.join(targetDir, "package.json"), packageJson);
216
245
  await writeJson(path.join(targetDir, "tsconfig.json"), createGeneratedTsConfig());
@@ -79,7 +79,15 @@ describe("generateToolcraft", () => {
79
79
  assert.equal(packageJson.devDependencies["@playwright/test"], "^1.51.1");
80
80
  assert.equal(packageJson.scripts["ai:check"], "node scripts/check-ai-skills.mjs");
81
81
  assert.equal(packageJson.scripts.dev, "node scripts/run-vite-on-free-port.mjs dev");
82
+ assert.equal(
83
+ packageJson.scripts["dev:restart"],
84
+ "node scripts/run-vite-on-free-port.mjs dev --toolcraft-restart",
85
+ );
82
86
  assert.equal(packageJson.scripts.preview, "node scripts/run-vite-on-free-port.mjs preview");
87
+ assert.equal(
88
+ packageJson.scripts["preview:restart"],
89
+ "node scripts/run-vite-on-free-port.mjs preview --toolcraft-restart",
90
+ );
83
91
  assert.equal(packageJson.scripts["docs:check"], "node scripts/check-toolcraft-docs.mjs");
84
92
  assert.equal(
85
93
  packageJson.scripts.test,
@@ -98,6 +106,9 @@ describe("generateToolcraft", () => {
98
106
  "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
99
107
  );
100
108
 
109
+ const indexHtmlSource = await fs.readFile(path.join(targetDir, "index.html"), "utf8");
110
+ assert.match(indexHtmlSource, /<title>Generated App<\/title>/);
111
+
101
112
  assert.ok(await fs.stat(path.join(targetDir, "playwright.config.ts")));
102
113
  const playwrightConfigSource = await fs.readFile(
103
114
  path.join(targetDir, "playwright.config.ts"),
@@ -121,6 +132,7 @@ describe("generateToolcraft", () => {
121
132
  assert.match(gitignoreSource, /dist/);
122
133
  assert.match(gitignoreSource, /playwright-report/);
123
134
  assert.match(gitignoreSource, /\.env\.\*/);
135
+ assert.match(gitignoreSource, /\.toolcraft/);
124
136
  await assert.rejects(() => fs.stat(path.join(targetDir, "gitignore")), /ENOENT/);
125
137
  assert.ok(await fs.stat(path.join(targetDir, "scripts/check-ai-skills.mjs")));
126
138
  assert.ok(await fs.stat(path.join(targetDir, "scripts/toolcraft-port.mjs")));
@@ -9,6 +9,21 @@ export function sanitizePackageName(value) {
9
9
  return sanitized || "toolcraft-app";
10
10
  }
11
11
 
12
+ export function normalizeProjectTitle(value) {
13
+ const words = String(value ?? "")
14
+ .trim()
15
+ .replace(/^@/, "")
16
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
17
+ .split(/[^a-zA-Z0-9]+/)
18
+ .filter(Boolean);
19
+
20
+ const title = words
21
+ .map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`)
22
+ .join(" ");
23
+
24
+ return title || "Toolcraft App";
25
+ }
26
+
12
27
  function isWorkspaceDependency(name, specifier) {
13
28
  return name.startsWith("@repo/") || String(specifier).startsWith("workspace:");
14
29
  }
@@ -1,7 +1,11 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
3
 
4
- import { createGeneratedPackageJson, sanitizePackageName } from "./package-json.mjs";
4
+ import {
5
+ createGeneratedPackageJson,
6
+ normalizeProjectTitle,
7
+ sanitizePackageName,
8
+ } from "./package-json.mjs";
5
9
 
6
10
  describe("sanitizePackageName", () => {
7
11
  it("normalizes package names for generated apps", () => {
@@ -11,6 +15,15 @@ describe("sanitizePackageName", () => {
11
15
  });
12
16
  });
13
17
 
18
+ describe("normalizeProjectTitle", () => {
19
+ it("creates a human-readable title from generated project names", () => {
20
+ assert.equal(normalizeProjectTitle("mesh-gradient"), "Mesh Gradient");
21
+ assert.equal(normalizeProjectTitle("mesh_gradient.app"), "Mesh Gradient App");
22
+ assert.equal(normalizeProjectTitle("@pixel-point/mesh-gradient"), "Pixel Point Mesh Gradient");
23
+ assert.equal(normalizeProjectTitle(""), "Toolcraft App");
24
+ });
25
+ });
26
+
14
27
  describe("createGeneratedPackageJson", () => {
15
28
  it("uses the starter package manifest as source of truth", () => {
16
29
  const packageJson = createGeneratedPackageJson({
@@ -82,6 +82,12 @@ describe("Toolcraft template component contracts", () => {
82
82
 
83
83
  expect(segmented?.strictness).toBe("best-fit");
84
84
  expect(segmented?.acceptableAlternatives?.join(" ")).toMatch(/Select/i);
85
+ expect(segmented?.layoutConstraints).toContain(
86
+ "Segmented controls are full-width controls and must not be placed in two-column inline or half-width layout groups.",
87
+ );
88
+ expect(getToolcraftComponentContract("segmented").aiUsageRules).toContain(
89
+ "Do not place Segmented beside Switch, Color, Select, or another control in an inline row; use Select when a finite choice must occupy a half-width column.",
90
+ );
85
91
  expect(select?.useWhen.join(" ")).toMatch(/long labels|many options/i);
86
92
  expect(select?.layoutConstraints).toContain(
87
93
  "Prefer compact two-column inline layout for related short Select pairs that tune one workflow or entity.",
@@ -150,6 +156,15 @@ describe("Toolcraft template component contracts", () => {
150
156
  expect(contract.aiUsageRules).toContain(
151
157
  'Use the default vector variant for spatial values such as position, offset, direction, focus, anchor, and light direction.',
152
158
  );
159
+ expect(contract.aiUsageRules).toContain(
160
+ 'Default spatial vector pads use coordinateMode: "screen": dragging left/up makes vector.x and vector.y smaller so canvas objects move left/up without renderer-side Y inversion.',
161
+ );
162
+ expect(contract.aiUsageRules).toContain(
163
+ "Holding Shift while dragging a vector pad locks movement to the dominant axis; do not build a custom pad just to support axis-constrained movement.",
164
+ );
165
+ expect(contract.aiUsageRules).toContain(
166
+ 'Use coordinateMode: "cartesian" only when the product intentionally exposes mathematical Y-up coordinates instead of canvas/screen movement.',
167
+ );
153
168
  expect(contract.aiUsageRules).toContain(
154
169
  "Vector is a compound control; acceptance must prove vector.x and vector.y both affect the product output.",
155
170
  );
@@ -192,7 +207,7 @@ describe("Toolcraft template component contracts", () => {
192
207
  "When the nearest section title already names the switch context, do not duplicate that title as the visible switch label. Use label false for a visual-only toggle and keep the meaning in target/description.",
193
208
  );
194
209
  expect(switchContract.aiUsageRules).toContain(
195
- 'A Switch may share an inline row with one related parameter control when the visible switch label is short enough to fit. That row uses equal-width columns; never shrink the switch column to intrinsic width. In section-owned rows, use a short visible label such as "Include" instead of repeating the section title, such as "Include background" inside Background.',
210
+ 'A Switch may share an inline row with one related parameter control when the visible switch label is short enough to fit. That row uses equal-width columns and the same horizontal column gap as paired Select controls; never shrink the switch column to intrinsic width. The non-switch parameter uses label false in that row; if its label is needed, stack the controls instead. In section-owned rows, use a short visible switch label such as "Include" instead of repeating the section title, such as "Include background" inside Background.',
196
211
  );
197
212
  expect(checkboxContract.aiUsageRules).toContain(
198
213
  'Checkbox labels name the setting context only; do not prefix labels with "Enable" or "Disable" because the checkbox already communicates enabled/selected state.',
@@ -207,7 +222,7 @@ describe("Toolcraft template component contracts", () => {
207
222
  "Two adjacent Checkbox controls for the same product entity must share one inline row when every visible label fits without truncation. Keep paired labels to short one- or two-word names; the runtime auto-pairs safe adjacent checkboxes by target entity, and generated schemas should stack checkboxes only when any label would truncate.",
208
223
  );
209
224
  expect(checkboxContract.aiUsageRules).toContain(
210
- "A Checkbox may share an inline row with one related parameter control when the visible checkbox label is short enough to fit. That row uses equal-width columns; never shrink the checkbox column to intrinsic width. Hide the checkbox label when the section title provides the visible context.",
225
+ "A Checkbox may share an inline row with one related parameter control when the visible checkbox label is short enough to fit. That row uses equal-width columns and the same horizontal column gap as paired Select controls; never shrink the checkbox column to intrinsic width. The non-checkbox parameter uses label false in that row; if its label is needed, stack the controls instead. Hide the checkbox label when the section title provides the visible context.",
211
226
  );
212
227
  });
213
228
 
@@ -224,6 +239,12 @@ describe("Toolcraft template component contracts", () => {
224
239
  expect(contract.aiUsageRules).toContain(
225
240
  "Do not use a custom control to recreate a built-in Slider, RangeSlider, Select, Segmented, Switch, Checkbox, Color, ColorOpacity, Gradient, FontPicker, ImagePicker, FileDrop, TextInput, CodeTextarea, RangeInput, Palette, Actions, CollectionActions, Curves, AnchorGrid, ChannelMixer, Vector, or PanelActions control.",
226
241
  );
242
+ expect(contract.aiUsageRules).toContain(
243
+ "When a custom control owns a growable, removable, selectable, or reorderable runtime item set, its builtInFitCheck must explicitly check collectionActions and actions before choosing custom; this is based on the value model and user workflow, not on entity names such as masks or glyphs.",
244
+ );
245
+ expect(contract.aiUsageRules).toContain(
246
+ "Do not justify custom controls with icons, layout, styling, compactness, or custom buttons alone. The fit check must name the product interaction or value model that built-ins cannot express.",
247
+ );
227
248
  expect(contract.aiUsageRules).toContain(
228
249
  "Custom controls may use Toolcraft primitives for small app-specific chrome, but must not import or render low-level runtime surfaces or duplicate toolbar, timeline, layers, canvas, panel, or built-in control mechanics.",
229
250
  );
@@ -254,6 +275,30 @@ describe("Toolcraft template component contracts", () => {
254
275
  expect(contract.aiUsageRules).toContain(
255
276
  "Do not use Actions for animation transport; Play, Pause, Resume, Restart, and Scrub belong to the top timeline when timeline behavior exists.",
256
277
  );
278
+ expect(contract.decisionCatalog?.layoutConstraints).toContain(
279
+ "Do not set an Actions control label to the exact same visible text as its only button; use a short one- or two-word context label such as Ink wash, Palette action, or Current layer while the button keeps the command verb.",
280
+ );
281
+ expect(contract.decisionCatalog?.layoutConstraints).toContain(
282
+ "Actions never use a side-label layout. If a visible label exists, it sits above the buttons.",
283
+ );
284
+ expect(contract.decisionCatalog?.layoutConstraints).toContain(
285
+ "Actions buttons render as a two-column grid. One visible button occupies the left half of the section; two buttons occupy one half each; more than two buttons wrap into additional 50% cells.",
286
+ );
287
+ expect(contract.decisionCatalog?.layoutConstraints).toContain(
288
+ "Do not center or right-align a partial final Actions row; an odd trailing button stays in the left 50% cell.",
289
+ );
290
+ expect(contract.aiUsageRules).toContain(
291
+ "For a single visible Actions button, the control label and button label must not be identical; make the control label a concise context and the button label the command.",
292
+ );
293
+ expect(contract.aiUsageRules).toContain(
294
+ "Render the Actions label above the buttons; do not put the label on the left with buttons on the right.",
295
+ );
296
+ expect(contract.aiUsageRules).toContain(
297
+ "Render Actions buttons in 50% cells: one button uses the left half, two buttons fill one row, and larger groups continue in two columns.",
298
+ );
299
+ expect(contract.aiUsageRules).toContain(
300
+ "Do not stretch an odd trailing Actions button full-width.",
301
+ );
257
302
  expect(contract.aiUsageRules).toContain(
258
303
  'For local reset-like actions, use product-specific values such as "reset-current-layer" or "reset-palette" and handle them through ToolcraftApp onPanelAction; do not use a bare "reset" value unless the action intentionally runs controls.reset.',
259
304
  );
@@ -286,6 +331,10 @@ describe("Toolcraft template component contracts", () => {
286
331
  "recommendedMaxItems is advisory only and must not disable the plus button. Use hardMaxItems only when a real product, algorithm, API, export, or measured performance limit requires it.",
287
332
  );
288
333
  expect(contract.aiUsageRules.join(" ")).toMatch(/TextInput/);
334
+ expect(contract.aiUsageRules.join(" ")).toMatch(/FontPicker/);
335
+ expect(contract.aiUsageRules).toContain(
336
+ "Use FontPicker as the collection item control when each repeated item is a typography/text-style entity; do not split its font, color, opacity, size, case, letter-spacing, or line-height into sibling collection fields.",
337
+ );
289
338
  });
290
339
 
291
340
  it("documents Palette as a constrained design-token color control", () => {
@@ -358,13 +407,16 @@ describe("Toolcraft template component contracts", () => {
358
407
  "Use intrinsic-media for single-layer upload/generation apps so imported media natural size becomes canvas.size.",
359
408
  );
360
409
  expect(contract.aiUsageRules).toContain(
361
- "Use editable-output by default for generated, exportable, shader, poster, badge, wall, banner, thumbnail, and product-output apps where users should see or edit width and height.",
410
+ "Use editable-output for generated, exportable, shader, poster, badge, wall, banner, thumbnail, procedural, reference-clone, and product-output apps so users always see Aspect ratio, Canvas width, and Canvas height.",
362
411
  );
363
412
  expect(contract.aiUsageRules).toContain(
364
- "A user-provided base/default size is not a reason to remove size controls; model it as canvas.size plus editable-output unless the prompt or reference explicitly locks output dimensions.",
413
+ "A user-provided, reference, fixed-format, or base/default size is not a reason to remove size controls; model it as canvas.size plus editable-output so the size is an initial value, not a hidden lock.",
365
414
  );
366
415
  expect(contract.aiUsageRules).toContain(
367
- "Use fixed-output only when the product output size must not be user-editable, and prove that lock with canvasSizingCoverage fixed-output-size acceptance.",
416
+ "Do not use fixed-output for generated product/output apps with export actions. Reserve fixed-output for non-product internal fixtures where width and height truly must never be user-editable, and prove that lock with canvasSizingCoverage fixed-output-size acceptance.",
417
+ );
418
+ expect(contract.aiUsageRules).toContain(
419
+ "A reference or previous app lacking a size editor, or defining a fixed-size baseline, is not a fixed-output reason for a generated product app; product-output clones still use editable-output.",
368
420
  );
369
421
  expect(contract.aiUsageRules).toContain(
370
422
  "Resolved canvas.size exists for every canvas app, but visible Canvas width and Canvas height controls are mandatory only for editable-output sizing and do not depend on settingsTransfer.",
@@ -382,7 +434,7 @@ describe("Toolcraft template component contracts", () => {
382
434
  "Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
383
435
  );
384
436
  expect(contract.aiUsageRules).toContain(
385
- "For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from 1x to 2x without changing visible canvas size, and adding/enabling it requires targeted browser evidence that the canvas stays responsive at the selected scale.",
437
+ "For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from 1 to 2 without changing visible canvas size, and adding/enabling it requires targeted browser evidence that the canvas stays responsive at the selected scale.",
386
438
  );
387
439
  expect(contract.aiUsageRules).toContain(
388
440
  "After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
@@ -448,6 +500,24 @@ describe("Toolcraft template component contracts", () => {
448
500
  const slider = getToolcraftComponentContract("slider");
449
501
  const rangeSlider = getToolcraftComponentContract("rangeSlider");
450
502
 
503
+ expect(slider.decisionCatalog.requiredAcceptance).toContain(
504
+ "Prove dragging the slider changes product output or the intended runtime side effect while the drag is in progress, not only after pointer release, blur, Apply, or a final commit.",
505
+ );
506
+ expect(slider.aiUsageRules).toContain(
507
+ "Sliders are live canvas controls: dragging must update runtime state and product output in real time by default.",
508
+ );
509
+ expect(slider.aiUsageRules).toContain(
510
+ "Do not implement slider values as deferred local drafts, Apply-only updates, pointer-up-only commits, or renderer changes that appear only after the user asks again.",
511
+ );
512
+ expect(slider.aiUsageRules).toContain(
513
+ "Slider performance coverage must use a real control-drag scenario; control-change coverage is not enough to prove live canvas feedback or drag smoothness.",
514
+ );
515
+ expect(slider.aiUsageRules).toContain(
516
+ "If a live slider causes jank, optimize the renderer path first: update uniforms or stable buffers, cache expensive inputs, coalesce preview work to requestAnimationFrame, cancel stale async renders, move heavy work off React, or switch renderer strategy.",
517
+ );
518
+ expect(slider.aiUsageRules).toContain(
519
+ "Only in an extreme documented performance ceiling may a slider use a degraded live preview or delayed heavy refinement; the user must still see immediate canvas feedback while dragging and the worklog must record the measured reason.",
520
+ );
451
521
  expect(slider.aiUsageRules).toContain(
452
522
  "Slider step means numeric snapping only; it does not make the slider visually discrete by itself.",
453
523
  );
@@ -464,16 +534,19 @@ describe("Toolcraft template component contracts", () => {
464
534
  "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
465
535
  );
466
536
  expect(slider.aiUsageRules).toContain(
467
- "Use slider unit only for measurement or scale suffixes such as %, px, °, x, s, ms, fps, rows/cols, or similar domain units.",
537
+ "Use slider unit only for real measurement suffixes such as %, px, °, s, ms, fps, rows/cols, or similar domain units.",
468
538
  );
469
539
  expect(slider.aiUsageRules).toContain(
470
540
  "Do not use unit for repeated entity nouns already named by the section or label, such as Letters + letters, Shape Density / Count + shapes, Words + words, Symbols + symbols, Items + items, Particles + particles, or Layers + layers.",
471
541
  );
542
+ expect(slider.aiUsageRules).toContain(
543
+ 'Do not use unit: "x"; scale, multiplier, intensity, opacity, strength, depth, and shader amount sliders display plain numbers unless a real measurement unit applies.',
544
+ );
472
545
  expect(slider.aiUsageRules).toContain(
473
546
  "When the value needs an entity noun to make sense, improve the label or section title instead of appending that noun as the value unit.",
474
547
  );
475
548
  expect(slider.aiUsageRules).toContain(
476
- "Compact symbol/CSS units render tight, such as 70%, 24px, 1.2x, and 8s; word units render with a space, such as 5 cols, when they are truly needed.",
549
+ "Compact symbol/CSS units render tight, such as 70%, 24px, and 8s; word units render with a space, such as 5 cols, when they are truly needed.",
477
550
  );
478
551
  expect(slider.aiUsageRules).toContain(
479
552
  "Slider valueLabel is editable only when it contains a numeric value; textual state labels such as Normal are display-only and must not expose hover or click editing affordances.",
@@ -499,6 +572,24 @@ describe("Toolcraft template component contracts", () => {
499
572
  expect(slider.aiUsageRules).toContain(
500
573
  "Do not leave a mode-dependent slider active while making the renderer ignore it; the UI must expose the unavailable state.",
501
574
  );
575
+ expect(rangeSlider.decisionCatalog.requiredAcceptance).toContain(
576
+ "Prove dragging rangeSlider.lower and rangeSlider.upper both affect product output while the drag is in progress, not only after pointer release, blur, Apply, or a final commit.",
577
+ );
578
+ expect(rangeSlider.aiUsageRules).toContain(
579
+ "Range sliders are live canvas controls: dragging either thumb must update runtime state and product output in real time by default.",
580
+ );
581
+ expect(rangeSlider.aiUsageRules).toContain(
582
+ "Do not implement range slider values as deferred local drafts, Apply-only updates, pointer-up-only commits, or renderer changes that appear only after the user asks again.",
583
+ );
584
+ expect(rangeSlider.aiUsageRules).toContain(
585
+ "Range slider performance coverage must use a real control-drag scenario; control-change coverage is not enough to prove live canvas feedback or drag smoothness.",
586
+ );
587
+ expect(rangeSlider.aiUsageRules).toContain(
588
+ "If a live range slider causes jank, optimize the renderer path first: update uniforms or stable buffers, cache expensive inputs, coalesce preview work to requestAnimationFrame, cancel stale async renders, move heavy work off React, or switch renderer strategy.",
589
+ );
590
+ expect(rangeSlider.aiUsageRules).toContain(
591
+ "Only in an extreme documented performance ceiling may a range slider use a degraded live preview or delayed heavy refinement; the user must still see immediate canvas feedback while dragging and the worklog must record the measured reason.",
592
+ );
502
593
  expect(rangeSlider.aiUsageRules).toContain(
503
594
  "Range slider step means numeric snapping only; it does not make the range slider visually discrete by itself.",
504
595
  );
@@ -515,7 +606,7 @@ describe("Toolcraft template component contracts", () => {
515
606
  "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
516
607
  );
517
608
  expect(rangeSlider.aiUsageRules).toContain(
518
- "Use rangeSlider unit only for measurement or scale suffixes; do not use it for repeated entity nouns already named by the section or label.",
609
+ "Use rangeSlider unit only for real measurement suffixes; do not use it for repeated entity nouns already named by the section or label, and do not use x as a unit.",
519
610
  );
520
611
  expect(rangeSlider.aiUsageRules).toContain(
521
612
  "When a range label needs an entity noun to make sense, improve the label or section title instead of appending that noun as the value unit.",
@@ -568,22 +659,31 @@ describe("Toolcraft template component contracts", () => {
568
659
  'Pair renderer-owned output background color with export.includeBackground in one Background section directly before export settings. Use an equal-width inline row with the export.includeBackground Switch labeled "Include" on the left and the background Color parameter with label false on the right; each control occupies one half of the row.',
569
660
  );
570
661
  expect(color.aiUsageRules).toContain(
571
- "Preview, PNG export, and video export must read the runtime background color value instead of hardcoding that background in CSS, Canvas fillStyle, or WebGL clearColor. export.includeBackground controls only PNG alpha; it must not make live preview, workspace canvas backing, or video transparent.",
662
+ "Preview, PNG export, and video export must read the runtime background color value instead of hardcoding that background in CSS, Canvas fillStyle, or WebGL clearColor. export.includeBackground controls live preview product background and PNG alpha; it must not make the Toolcraft canvas shell/backing or video output transparent.",
572
663
  );
573
664
  expect(color.aiUsageRules).toContain(
574
665
  "When one short numeric/text field and one Color field configure the same entity, keep them in one two-column inline layout group.",
575
666
  );
576
667
  expect(color.aiUsageRules).toContain(
577
- 'Mixed inline rows require visible labels on both controls. The required Background row is the only section-title-owned exception: use the Switch label "Include" and set the background Color control label to false. Color fields in other mixed rows must not be unlabeled.',
668
+ 'Mixed inline rows usually require visible labels on both controls. Toggle-plus-parameter rows are the section-owned exception: keep the Switch/Checkbox label visible and set the non-toggle parameter label to false; if the parameter label is needed, stack the controls instead. All 50/50 inline rows use the same horizontal column gap as paired Select controls. The required Background row uses the Switch label "Include" and sets the background Color control label to false. Palette variation color banks are the other exception when the group/section label already names the color bank.',
578
669
  );
579
670
  expect(color.aiUsageRules).toContain(
580
671
  "Plain Color popovers must not show opacity controls. If opacity is editable, use ColorOpacity instead.",
581
672
  );
582
673
  expect(color.aiUsageRules).toContain(
583
- "Show visible labels for Color and ColorOpacity controls inside mixed sections that contain any non-color controls.",
674
+ "Decide color label visibility from the user's point of view. Omit labels for color banks that only add palette variety, such as Accent Shades, Bead Colors, or palette.accent1..5.",
675
+ );
676
+ expect(color.aiUsageRules).toContain(
677
+ "Make color label visibility a group-level decision: do not mix labeled and unlabeled items inside one semantic color bank.",
584
678
  );
585
679
  expect(color.aiUsageRules).toContain(
586
- "Omit visible color field labels only when the section contains color controls and no other control types.",
680
+ "Keep visible labels when colors edit distinct user-facing entities or roles, such as Fill, Stroke, Background, Connector, Object, or Highlight.",
681
+ );
682
+ expect(color.aiUsageRules).toContain(
683
+ "A color bank can share a section with distribution controls such as Spread, Mix, or Randomness and still keep each color item unlabeled when the section title names the palette context.",
684
+ );
685
+ expect(color.aiUsageRules).toContain(
686
+ "If a multi-color bank has an odd trailing plain Color, keep that last Color at half width; only ColorOpacity or intentionally full-width compound controls occupy a full row.",
587
687
  );
588
688
  expect(gradient.aiUsageRules).toContain(
589
689
  "Gradient is a compound control; acceptance must prove gradient.gradientType, gradient.angle, gradient.stops.position, gradient.stops.color, and gradient.stops.opacity all affect the product output when visible.",
@@ -600,6 +700,9 @@ describe("Toolcraft template component contracts", () => {
600
700
  expect(fontPicker.aiUsageRules).toContain(
601
701
  'Do not recreate FontPicker with a plain Select plus separate sliders; use type: "fontPicker" so the popup mechanics and footer controls stay intact.',
602
702
  );
703
+ expect(fontPicker.aiUsageRules).toContain(
704
+ "FontPicker standard/default text color is #FFFFFF with opacity 100; omit color/opacity or use those values unless the prompt or reference explicitly requires a different initial text color.",
705
+ );
603
706
  expect(fontPicker.aiUsageRules).toContain(
604
707
  "Any product text controlled by FontPicker must render fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, and opacity in preview and export; do not leave typography values as panel-only runtime state.",
605
708
  );
@@ -743,14 +846,20 @@ describe("Toolcraft template component contracts", () => {
743
846
  "Custom renderers must define performance budgets for media import, preview updates, control drags, and export/copy before implementation.",
744
847
  );
745
848
  expect(contract.aiUsageRules).toContain(
746
- "Controls that change renderer workload, such as Char Size, Grid Density, Matrix Scale, Sample Count, Resolution, Blur Radius, Iterations, Particle Count, or Quality, must be tested at min, default, and max values.",
849
+ "Controls that change renderer workload by changing output dimensions, element count, density, sample count, iteration count, blur/filter radius, shader branch cost, media processing, text layout, or export quality must be tested at min, default, and max values.",
747
850
  );
748
851
  expect(contract.aiUsageRules).toContain(
749
- "Hash differs is not enough for workload controls; tests must assert semantic direction, for example smaller Char Size increases glyph/cell density and larger Char Size decreases it.",
852
+ "For workload control scenarios, stressFixture is the tested control value. If the app has an independent heavy baseline such as large media, long text, many items, high render scale, or dense source state, declare workloadFixture and apply it before the measured control interaction.",
853
+ );
854
+ expect(contract.aiUsageRules).toContain(
855
+ "Hash differs is not enough for workload controls; tests must assert semantic direction, such as density increasing item count or size changes reducing/increasing rendered cells.",
750
856
  );
751
857
  expect(contract.aiUsageRules).toContain(
752
858
  "Performance tests must use representative fixtures and the same renderer/export path as the running app, not only tiny 32px fixtures or isolated helper state.",
753
859
  );
860
+ expect(contract.aiUsageRules).toContain(
861
+ 'Media-import workload fixtures and media workload baselines must use fixture kind "media" with numeric width and height at least 1920x1080-equivalent; 640x480 preview fixtures cannot satisfy upload, effect-control, or image-processing performance coverage.',
862
+ );
754
863
  expect(contract.aiUsageRules).toContain(
755
864
  "Slider drags and high-frequency controls must debounce or coalesce preview work, cancel stale async renders, and avoid re-decoding media on every control change.",
756
865
  );
@@ -790,8 +899,19 @@ describe("Toolcraft template component contracts", () => {
790
899
  expect(contract.aiUsageRules).toContain(
791
900
  'productRepresentation "mixed" is valid only when rendererTechnique.layers proves at least two different content families.',
792
901
  );
902
+ expect(contract.aiUsageRules).toContain(
903
+ "Custom renderer apps must declare rendererPipeline with render passes, cache keys, execution location, preview/export quality, and interaction invalidation before implementation.",
904
+ );
905
+ expect(contract.aiUsageRules).toContain(
906
+ "Render Pipeline Inventory must explain which runtime targets invalidate each expensive pass; high-frequency interactions such as animation frames, drag, zoom, pan, timeline playback, and mask movement must not invalidate upstream decode/preprocess/pixel-transform work unless that target truly changes the upstream result.",
907
+ );
908
+ expect(contract.aiUsageRules).toContain(
909
+ "Cache-sensitive render passes such as decode, preprocess, pixel-transform, text-layout, rasterize, and composite must declare cache keys so tests can reject full recomputation on every control change.",
910
+ );
793
911
  expect(contract.aiUsageRules.join("\n")).toMatch(/rendererTechnique/);
794
912
  expect(contract.aiUsageRules.join("\n")).toMatch(/rendererTechnique\.layers/);
913
+ expect(contract.aiUsageRules.join("\n")).toMatch(/rendererPipeline/);
914
+ expect(contract.aiUsageRules.join("\n")).toMatch(/Render Pipeline Inventory/);
795
915
  expect(contract.aiUsageRules.join("\n")).toMatch(/sourceRepresentation/);
796
916
  expect(contract.aiUsageRules.join("\n")).toMatch(/productRepresentation/);
797
917
  expect(contract.aiUsageRules.join("\n")).toMatch(/previewRenderer/);
@@ -809,11 +929,14 @@ describe("Toolcraft template component contracts", () => {
809
929
  "Text-output and vector-output visible previews must preserve native output fidelity. Do not render a low-resolution offscreen canvas or texture and upscale it to the product size.",
810
930
  );
811
931
  expect(contract.aiUsageRules).toContain(
812
- "Pixel-output renderers must use WebGL or WebGPU even when the scene is static.",
932
+ "Pixel-output renderers must treat WebGL/WebGPU as the default candidate even when the scene is static; Canvas 2D is allowed only when measured worst-case evidence shows the CPU path preserves quality and remains responsive.",
813
933
  );
814
934
  expect(contract.aiUsageRules).toContain(
815
935
  "Procedural pixel renderers, shader-like effects, animated mesh gradients, and large exportable previews should use WebGL or WebGPU for pixel work instead of main-thread ImageData loops.",
816
936
  );
937
+ expect(contract.aiUsageRules).toContain(
938
+ "Detail-heavy Canvas 2D pixel/media renderers may stay on CPU only when rendererTechnique records measured stress evidence for rejecting WebGL/WebGPU; if the heavy media stress fails, move pixel work to GPU instead of lowering quality.",
939
+ );
817
940
  expect(contract.aiUsageRules).toContain(
818
941
  "WebGL and WebGPU renderers must initialize contexts, programs, shaders, pipelines, textures, and large buffers once, then update uniforms or stable buffers when controls change.",
819
942
  );
@@ -830,11 +953,14 @@ describe("Toolcraft template component contracts", () => {
830
953
  "Animated preview renderers must suspend or coalesce non-essential animation work while the user drags, pans, pinches, zooms, or centers the canvas viewport, then resume from the correct timeline or autonomous time without changing the user's play/pause state.",
831
954
  );
832
955
  expect(contract.aiUsageRules).toContain(
833
- "If a generated app uses ImageData, getImageData, or putImageData for procedural output, performance validation must fail unless the renderer is converted to GPU rendering or the CPU path is removed.",
956
+ "If a generated app uses ImageData, getImageData, or putImageData for procedural output, performance validation must fail unless rendererTechnique records measured WebGL/WebGPU comparison evidence that the CPU path preserves quality and responsiveness, or the renderer is moved to GPU.",
834
957
  );
835
958
  expect(contract.aiUsageRules).toContain(
836
959
  "Performance matrices must declare rendererStrategy so tests can distinguish none, dom, svg, canvas-2d, webgl, and webgpu renderer paths.",
837
960
  );
961
+ expect(contract.aiUsageRules).toContain(
962
+ "If a renderer cannot meet the budget, first optimize renderer technique, caching, invalidation, scheduling, and critical-path work. Only change exposed product ranges, work units, or controls after recording measured evidence that the requested quality ceiling is impossible; do not silently reduce product quality to pass budgets.",
963
+ );
838
964
  });
839
965
 
840
966
  it("documents reference runtime clone mode as a tested composition contract", () => {
@@ -1156,10 +1282,10 @@ describe("Toolcraft template component contracts", () => {
1156
1282
  'Product-output apps must expose a dedicated "Background" section directly before the first export settings section. With PNG export that first section is Image Export; with video-only export it is Video Export.',
1157
1283
  );
1158
1284
  expect(contract.aiUsageRules).toContain(
1159
- "Product-output apps must pass the includeBackground runtime value to createToolcraftPngExportCanvas only for PNG alpha.",
1285
+ "Product-output apps must pass the includeBackground runtime value to createToolcraftPngExportCanvas for PNG alpha and call shouldIncludeToolcraftPreviewBackground(state) for live preview product background.",
1160
1286
  );
1161
1287
  expect(contract.aiUsageRules).toContain(
1162
- "PNG export must use createToolcraftPngExportCanvas so background transparency and selected image dimensions or retina fallback are applied consistently without making live preview, workspace canvas backing, or video transparent.",
1288
+ "PNG export must use createToolcraftPngExportCanvas so background transparency and selected image dimensions or retina fallback are applied consistently; turning Include off makes preview product background and PNG alpha transparent without hiding the Toolcraft canvas backing or video background.",
1163
1289
  );
1164
1290
  expect(contract.aiUsageRules).toContain(
1165
1291
  "Video export must keep product background and use getToolcraftRetinaExportSize for retina dimensions.",
@@ -1184,15 +1310,39 @@ describe("Toolcraft template component contracts", () => {
1184
1310
  it("documents file upload ownership across single-layer and multi-layer apps", () => {
1185
1311
  const contract = getToolcraftComponentContract("fileDrop");
1186
1312
 
1187
- expect(contract.commands).toEqual(["media.delete", "media.import"]);
1313
+ expect(contract.commands).toEqual(["media.delete", "media.import", "media.reorder"]);
1314
+ expect(contract.aiUsageRules).toContain(
1315
+ 'Use fileDrop with assetKind: "image" for image-only source media and assetKind: "file" for arbitrary uploaded files.',
1316
+ );
1317
+ expect(contract.aiUsageRules).toContain(
1318
+ "When uploaded/imported content is part of the source-material flow, the canvas must not show agent-invented artwork, CTA text, fake sample output, decorative placeholders, or preset source designs before real content exists; keep the canvas neutral/runtime-backed and put upload affordance in fileDrop.",
1319
+ );
1320
+ expect(contract.aiUsageRules).toContain(
1321
+ "Do not add procedural Source Preset modes only to avoid an empty canvas. A default procedural or reference source is allowed only when the prompt/reference explicitly defines it and the worklog records that evidence.",
1322
+ );
1188
1323
  expect(contract.aiUsageRules).toContain(
1189
1324
  "In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
1190
1325
  );
1326
+ expect(contract.aiUsageRules).toContain(
1327
+ "In file mode, the runtime shows uploaded files as a sortable list with paperclip icons, file names, remove buttons, and --border/5 separators.",
1328
+ );
1329
+ expect(contract.aiUsageRules).toContain(
1330
+ "In single-layer apps, global Reset controls and section reset must remove uploaded fileDrop source media and return the fileDrop target to defaultValue.",
1331
+ );
1191
1332
  expect(contract.aiUsageRules).toContain(
1192
1333
  "Use fileDrop with multiple: true when the app needs several uploaded images as one source set; do not build a custom thumbnail uploader for this.",
1193
1334
  );
1194
1335
  expect(contract.aiUsageRules).toContain(
1195
- "When multiple uploaded images are present, the runtime appends media, shows a four-column preview grid, puts the add-more tile last, and exposes per-image removal.",
1336
+ "When multiple uploaded images are present, the runtime appends media, shows a sortable four-column preview grid, puts the add-more tile last, and exposes per-image removal.",
1337
+ );
1338
+ expect(contract.aiUsageRules).toContain(
1339
+ "Canvas drops route to the first visible matching fileDrop target by asset kind: image files prefer image uploaders, non-image files prefer file uploaders, and file uploaders accept images only when no image uploader matches.",
1340
+ );
1341
+ expect(contract.aiUsageRules).toContain(
1342
+ "Dragging thumbnails reorders runtime mediaAssets; preview, export, and renderer mapping must consume that media order instead of maintaining a separate product-only order.",
1343
+ );
1344
+ expect(contract.aiUsageRules).toContain(
1345
+ "Do not create custom upload buttons, file lists, or file sorting for generic source uploads when fileDrop can represent the source set.",
1196
1346
  );
1197
1347
  expect(contract.aiUsageRules).toContain(
1198
1348
  "In multi-layer apps, deletion and visibility belong to the Layers panel; fileDrop remains an upload target.",
@@ -1226,6 +1376,9 @@ describe("Toolcraft template component contracts", () => {
1226
1376
  expect(contract.aiUsageRules).toContain(
1227
1377
  "Render multiple related color fields in one section with at most two colors per row.",
1228
1378
  );
1379
+ expect(contract.aiUsageRules).toContain(
1380
+ "If a multi-color bank has an odd trailing plain Color, keep that last Color at half width; only ColorOpacity or intentionally full-width compound controls occupy a full row.",
1381
+ );
1229
1382
  });
1230
1383
 
1231
1384
  it("documents CodeTextarea as generic multiline text input", () => {
@@ -1238,10 +1391,16 @@ describe("Toolcraft template component contracts", () => {
1238
1391
  "CodeTextarea is the multiline text input for any potentially long value, not only source code.",
1239
1392
  );
1240
1393
  expect(contract.aiUsageRules).toContain(
1241
- "Use text for short single-line strings such as names, small numeric values, compact prompts, titles, and short tokens.",
1394
+ "Do not use CodeTextarea for short single-line canvas text, button labels, names, titles, captions, badges, or short tokens; use TextInput.",
1242
1395
  );
1243
1396
  expect(contract.aiUsageRules).toContain(
1244
- "Use code when the user may enter long prompts, multiline text, JSON, CSS, shader code, scripts, templates, or other long structured data.",
1397
+ "Use text for short single-line strings such as names, button labels, small numeric values, compact prompts, titles, captions, and short tokens.",
1398
+ );
1399
+ expect(contract.aiUsageRules).toContain(
1400
+ "Use code only when the user may enter long prompts, multiline text, instructions, JSON, CSS, shader code, scripts, templates, or other long structured data.",
1401
+ );
1402
+ expect(contract.aiUsageRules).toContain(
1403
+ "If CodeTextarea has a short single-line default value, the schema description must make the long, multiline, or structured-content reason explicit.",
1245
1404
  );
1246
1405
  expect(contract.aiUsageRules).toContain(
1247
1406
  "CodeTextarea is a content editor and applies values while typing; do not wait for blur, Enter, or Cmd/Ctrl+Enter to update runtime state.",
@@ -1257,6 +1416,9 @@ describe("Toolcraft template component contracts", () => {
1257
1416
  expect(contract.visualComponent).toBe("TextInput");
1258
1417
  expect(contract.defaultSectionLayout).toBe("grouped");
1259
1418
  expect(contract.labelPolicy).toBe("required");
1419
+ expect(contract.aiUsageRules).toContain(
1420
+ "TextInput owns short single-line product text: button labels, labels on the canvas, names, titles, captions, badges, short tokens, and compact prompts.",
1421
+ );
1260
1422
  expect(contract.aiUsageRules).toContain(
1261
1423
  'TextInput commitMode defaults to "content": text content, prompts, names, tokens, titles, and instructions apply while typing.',
1262
1424
  );