@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixel-point/toolcraft",
3
- "version": "0.0.8",
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
 
@@ -261,10 +276,28 @@ describe("Toolcraft template component contracts", () => {
261
276
  "Do not use Actions for animation transport; Play, Pause, Resume, Restart, and Scrub belong to the top timeline when timeline behavior exists.",
262
277
  );
263
278
  expect(contract.decisionCatalog?.layoutConstraints).toContain(
264
- "When an Actions button group wraps below its label, the wrapped button row starts at the left edge of the control content instead of centering or staying right-aligned.",
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.",
265
289
  );
266
290
  expect(contract.aiUsageRules).toContain(
267
- "When an Actions button group no longer fits beside its label and wraps to a second row, align the wrapped button row to the left.",
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.",
268
301
  );
269
302
  expect(contract.aiUsageRules).toContain(
270
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.',
@@ -374,13 +407,16 @@ describe("Toolcraft template component contracts", () => {
374
407
  "Use intrinsic-media for single-layer upload/generation apps so imported media natural size becomes canvas.size.",
375
408
  );
376
409
  expect(contract.aiUsageRules).toContain(
377
- "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.",
411
+ );
412
+ expect(contract.aiUsageRules).toContain(
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.",
378
414
  );
379
415
  expect(contract.aiUsageRules).toContain(
380
- "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.",
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.",
381
417
  );
382
418
  expect(contract.aiUsageRules).toContain(
383
- "Use fixed-output only when the product output size must not be user-editable, and prove that lock with canvasSizingCoverage fixed-output-size acceptance.",
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.",
384
420
  );
385
421
  expect(contract.aiUsageRules).toContain(
386
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.",
@@ -398,7 +434,7 @@ describe("Toolcraft template component contracts", () => {
398
434
  "Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
399
435
  );
400
436
  expect(contract.aiUsageRules).toContain(
401
- "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.",
402
438
  );
403
439
  expect(contract.aiUsageRules).toContain(
404
440
  "After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
@@ -464,6 +500,24 @@ describe("Toolcraft template component contracts", () => {
464
500
  const slider = getToolcraftComponentContract("slider");
465
501
  const rangeSlider = getToolcraftComponentContract("rangeSlider");
466
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
+ );
467
521
  expect(slider.aiUsageRules).toContain(
468
522
  "Slider step means numeric snapping only; it does not make the slider visually discrete by itself.",
469
523
  );
@@ -480,16 +534,19 @@ describe("Toolcraft template component contracts", () => {
480
534
  "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
481
535
  );
482
536
  expect(slider.aiUsageRules).toContain(
483
- "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.",
484
538
  );
485
539
  expect(slider.aiUsageRules).toContain(
486
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.",
487
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
+ );
488
545
  expect(slider.aiUsageRules).toContain(
489
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.",
490
547
  );
491
548
  expect(slider.aiUsageRules).toContain(
492
- "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.",
493
550
  );
494
551
  expect(slider.aiUsageRules).toContain(
495
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.",
@@ -515,6 +572,24 @@ describe("Toolcraft template component contracts", () => {
515
572
  expect(slider.aiUsageRules).toContain(
516
573
  "Do not leave a mode-dependent slider active while making the renderer ignore it; the UI must expose the unavailable state.",
517
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
+ );
518
593
  expect(rangeSlider.aiUsageRules).toContain(
519
594
  "Range slider step means numeric snapping only; it does not make the range slider visually discrete by itself.",
520
595
  );
@@ -531,7 +606,7 @@ describe("Toolcraft template component contracts", () => {
531
606
  "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
532
607
  );
533
608
  expect(rangeSlider.aiUsageRules).toContain(
534
- "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.",
535
610
  );
536
611
  expect(rangeSlider.aiUsageRules).toContain(
537
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.",
@@ -590,7 +665,7 @@ describe("Toolcraft template component contracts", () => {
590
665
  "When one short numeric/text field and one Color field configure the same entity, keep them in one two-column inline layout group.",
591
666
  );
592
667
  expect(color.aiUsageRules).toContain(
593
- 'Mixed inline rows require visible labels on both controls. The required Background row is the section-title-owned exception: use the Switch label "Include" and set 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.',
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.',
594
669
  );
595
670
  expect(color.aiUsageRules).toContain(
596
671
  "Plain Color popovers must not show opacity controls. If opacity is editable, use ColorOpacity instead.",
@@ -1239,6 +1314,12 @@ describe("Toolcraft template component contracts", () => {
1239
1314
  expect(contract.aiUsageRules).toContain(
1240
1315
  'Use fileDrop with assetKind: "image" for image-only source media and assetKind: "file" for arbitrary uploaded files.',
1241
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
+ );
1242
1323
  expect(contract.aiUsageRules).toContain(
1243
1324
  "In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
1244
1325
  );
@@ -1310,10 +1391,16 @@ describe("Toolcraft template component contracts", () => {
1310
1391
  "CodeTextarea is the multiline text input for any potentially long value, not only source code.",
1311
1392
  );
1312
1393
  expect(contract.aiUsageRules).toContain(
1313
- "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.",
1395
+ );
1396
+ expect(contract.aiUsageRules).toContain(
1397
+ "Use text for short single-line strings such as names, button labels, small numeric values, compact prompts, titles, captions, and short tokens.",
1314
1398
  );
1315
1399
  expect(contract.aiUsageRules).toContain(
1316
- "Use code when the user may enter long prompts, multiline text, JSON, CSS, shader code, scripts, templates, or other long structured data.",
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.",
1317
1404
  );
1318
1405
  expect(contract.aiUsageRules).toContain(
1319
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.",
@@ -1329,6 +1416,9 @@ describe("Toolcraft template component contracts", () => {
1329
1416
  expect(contract.visualComponent).toBe("TextInput");
1330
1417
  expect(contract.defaultSectionLayout).toBe("grouped");
1331
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
+ );
1332
1422
  expect(contract.aiUsageRules).toContain(
1333
1423
  'TextInput commitMode defaults to "content": text content, prompts, names, tokens, titles, and instructions apply while typing.',
1334
1424
  );