@pixel-point/toolcraft 0.0.11 → 0.0.13
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.
- package/README.md +4 -2
- package/package.json +2 -1
- package/src/cli.mjs +19 -44
- package/src/cli.test.mjs +146 -8
- package/src/command-runner.mjs +71 -0
- package/src/dependency-install.mjs +87 -0
- package/src/generate.mjs +32 -4
- package/src/generate.test.mjs +72 -11
- package/src/package-json.mjs +11 -2
- package/src/package-json.test.mjs +27 -0
- package/src/package-manager.mjs +123 -0
- package/src/package-manager.test.mjs +80 -0
- package/templates/runtime/contracts/component-contracts.test.ts +28 -3
- package/templates/runtime/contracts/component-contracts.ts +12 -8
- package/templates/runtime/react/controls-panel.test.tsx +4 -0
- package/templates/starter/AGENTS.md +2 -2
- package/templates/starter/docs/toolcraft/acceptance-testing.md +3 -1
- package/templates/starter/docs/toolcraft/assembly-workflow.md +1 -1
- package/templates/starter/docs/toolcraft/component-rules.md +10 -1
- package/templates/starter/docs/toolcraft/performance.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +1 -1
- package/templates/starter/e2e/app-performance.spec.ts +3 -3
- package/templates/starter/package.json +3 -2
- package/templates/starter/scripts/run-vite-on-free-port.mjs +25 -6
- package/templates/starter/src/app/starter-acceptance.test.ts +15 -7
- package/templates/starter/src/app/starter-performance.test.ts +37 -9
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +18 -6
|
@@ -167,7 +167,7 @@ Animation checks should sample enough frames to catch jank. Interaction budgets
|
|
|
167
167
|
|
|
168
168
|
Use `app-performance.ts` as the single budget and fixture source. Browser performance tests must call `getToolcraftPerformanceStressValue(appPerformance, scenarioId)` for workload values and `expectToolcraftScenarioPerformanceBudget(..., appPerformance, scenarioId)` for budgets.
|
|
169
169
|
|
|
170
|
-
Run targeted performance scenarios for Tier 3 performance-sensitive edits when they directly exercise the touched workload, viewport, or export path. The full performance checkpoint prefers the current AI agent's controlled browser. `pnpm verify:perf` is the Playwright fallback suite; it runs
|
|
170
|
+
Run targeted performance scenarios for Tier 3 performance-sensitive edits when they directly exercise the touched workload, viewport, or export path. The default `pnpm test:browser` / `pnpm verify:ui` gate excludes every Playwright test whose name contains `browser perf:`, including the performance audit and budget scenarios. The full performance checkpoint prefers the current AI agent's controlled browser. `pnpm verify:perf` is the Playwright fallback suite; it runs every `browser perf:` scenario with one worker so budget failures are not hidden or created by unrelated parallel browser tests.
|
|
171
171
|
|
|
172
172
|
Run a full performance checkpoint only when:
|
|
173
173
|
|
|
@@ -201,7 +201,7 @@ For `slider` and `rangeSlider`, `unit` is a real measurement suffix, not the ent
|
|
|
201
201
|
|
|
202
202
|
`slider` and `rangeSlider` are live controls. Dragging must update runtime state and product output while the drag is in progress, not only on pointer release, blur, Apply, or a final commit. Treat a non-live slider as a broken product mapping unless an extreme measured performance ceiling is documented; even then, keep immediate lightweight canvas feedback and refine the heavy output after coalescing/caching.
|
|
203
203
|
|
|
204
|
-
For `vector`, the default/spatial variant uses `coordinateMode: "screen"` by default: dragging left/up lowers `x` and `y`, so canvas objects move left/up without renderer-side Y inversion. Use `coordinateMode: "cartesian"` only for intentional mathematical Y-up coordinates. Color variants keep their color-axis semantics by default. Vector pad value labels show compact rounded coordinates; raw floating-point tails must never appear in the controls panel. Double-clicking the pad resets both axes to the control default through the normal runtime value update, matching the section header reset; if no default is defined, the fallback is `0,0`. Holding Shift while dragging locks movement to the dominant axis and must not select text or page content.
|
|
204
|
+
For `vector`, the default/spatial variant uses `coordinateMode: "screen"` by default: dragging left/up lowers `x` and `y`, so canvas objects move left/up without renderer-side Y inversion. Use it only for stable user-authored two-axis parameters. Do not expose Vector for current animation state, keyboard movement, pointer movement, physics state, timeline phase, velocity, target pose, current pose, or simulated position/direction just because the internal value has `x` and `y`. Use `coordinateMode: "cartesian"` only for intentional mathematical Y-up coordinates. Color variants keep their color-axis semantics by default. Vector pad value labels show compact rounded coordinates; raw floating-point tails must never appear in the controls panel. Double-clicking the pad resets both axes to the control default through the normal runtime value update, matching the section header reset; if no default is defined, the fallback is `0,0`. Holding Shift while dragging locks movement to the dominant axis and must not select text or page content.
|
|
205
205
|
|
|
206
206
|
Use `collectionActions` when the product owns a growable/shrinkable item list. `minItems` protects the smallest valid output, `recommendedMaxItems` is only a design recommendation, and `hardMaxItems` is valid only for a real product or technical limit. Adding/removing items must update the runtime array and the renderer/export must consume that same array. Do not pair a count slider with hidden fixed item controls when the user needs to add or remove actual entities. The collection label is on the left and remove/add buttons stay on the right. Homogeneous repeated items do not show visible per-item labels when the collection label already names the group. `itemControl.type` supports normal item built-ins such as `color`, `colorOpacity`, `text`, `select`, `segmented`, `slider`, `switch`, `checkbox`, `rangeInput`, and `fontPicker`; item controls still follow normal density rules, so plain colors use equal 50% columns when they fit. Use `fontPicker` as the item control when each repeated item is a text style or typography entity; do not split its font, weight, size, case, color/opacity, letter spacing, or line height into sibling collection fields.
|
|
207
207
|
|
|
@@ -184,7 +184,7 @@ function getFirstMatchIndex(source: string, pattern: RegExp): number {
|
|
|
184
184
|
return match?.index ?? -1;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
test("browser performance matrix points at real fallback browser tests", () => {
|
|
187
|
+
test("browser perf: performance matrix points at real fallback browser tests", () => {
|
|
188
188
|
const browserTestSources = readSiblingBrowserTestSources();
|
|
189
189
|
|
|
190
190
|
for (const scenario of starterPerformance.scenarios) {
|
|
@@ -199,7 +199,7 @@ test("browser performance matrix points at real fallback browser tests", () => {
|
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
test("browser performance tests use real Toolcraft interactions", () => {
|
|
202
|
+
test("browser perf: performance tests use real Toolcraft interactions", () => {
|
|
203
203
|
if (starterPerformance.scenarios.length === 0) {
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
@@ -493,7 +493,7 @@ test("browser performance tests use real Toolcraft interactions", () => {
|
|
|
493
493
|
}
|
|
494
494
|
});
|
|
495
495
|
|
|
496
|
-
test("declared renderer layer selectors are present", async ({ page }) => {
|
|
496
|
+
test("browser perf: declared renderer layer selectors are present", async ({ page }) => {
|
|
497
497
|
if (!starterPerformance.usesCustomRenderer) {
|
|
498
498
|
return;
|
|
499
499
|
}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"preview:restart": "node scripts/run-vite-on-free-port.mjs preview --toolcraft-restart",
|
|
13
13
|
"docs:check": "node scripts/check-toolcraft-docs.mjs",
|
|
14
14
|
"test": "node scripts/check-toolcraft-docs.mjs && node scripts/check-toolcraft-integrity.mjs && node --test scripts/*.test.mjs && vitest run src --passWithNoTests",
|
|
15
|
-
"test:browser": "playwright install chromium && playwright test",
|
|
16
|
-
"test:browser:perf": "playwright install chromium && playwright test
|
|
15
|
+
"test:browser": "playwright install chromium && playwright test --grep-invert \"browser perf:\"",
|
|
16
|
+
"test:browser:perf": "playwright install chromium && playwright test --grep \"browser perf:\" --workers=1 --pass-with-no-tests",
|
|
17
17
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
18
18
|
"verify:quick": "pnpm ai:check && pnpm test",
|
|
19
19
|
"verify:ui": "pnpm test:browser",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"class-variance-authority": "^0.7.1",
|
|
35
35
|
"clsx": "^2.1.1",
|
|
36
36
|
"cmdk": "^1.1.1",
|
|
37
|
+
"cross-spawn": "^7.0.6",
|
|
37
38
|
"lucide-react": "^0.511.0",
|
|
38
39
|
"motion": "^11.16.3",
|
|
39
40
|
"react": "^19.2.0",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import spawn from "cross-spawn";
|
|
4
8
|
|
|
5
9
|
import {
|
|
6
10
|
findAvailablePort,
|
|
@@ -14,6 +18,8 @@ import {
|
|
|
14
18
|
} from "./toolcraft-port.mjs";
|
|
15
19
|
|
|
16
20
|
const viteCommand = process.argv[2] ?? "dev";
|
|
21
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const projectDir = join(scriptDir, "..");
|
|
17
23
|
const restartArg = "--toolcraft-restart";
|
|
18
24
|
const rawPassthroughArgs = process.argv.slice(3).filter((arg) => arg !== "--");
|
|
19
25
|
const restartSamePort =
|
|
@@ -49,7 +55,7 @@ if (savedPort) {
|
|
|
49
55
|
process.exit(0);
|
|
50
56
|
} catch (error) {
|
|
51
57
|
console.error(
|
|
52
|
-
`Saved Toolcraft port ${savedPort} is busy, but it is not serving this app. Use
|
|
58
|
+
`Saved Toolcraft port ${savedPort} is busy, but it is not serving this app. Use the restart script to reclaim the saved port, or remove .toolcraft/server-port.json to assign a new port. ${error?.message ?? ""}`.trim(),
|
|
53
59
|
);
|
|
54
60
|
process.exit(1);
|
|
55
61
|
}
|
|
@@ -66,12 +72,25 @@ if (!port) {
|
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
const
|
|
75
|
+
const viteBinPath = join(
|
|
76
|
+
projectDir,
|
|
77
|
+
"node_modules",
|
|
78
|
+
".bin",
|
|
79
|
+
process.platform === "win32" ? "vite.cmd" : "vite",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await access(viteBinPath);
|
|
84
|
+
} catch {
|
|
85
|
+
console.error(
|
|
86
|
+
"Vite is not installed. Run this app's dependency installation command before starting the dev server.",
|
|
87
|
+
);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
70
91
|
const child = spawn(
|
|
71
|
-
|
|
92
|
+
viteBinPath,
|
|
72
93
|
[
|
|
73
|
-
"exec",
|
|
74
|
-
"vite",
|
|
75
94
|
viteCommand,
|
|
76
95
|
"--host",
|
|
77
96
|
"127.0.0.1",
|
|
@@ -554,8 +554,12 @@ function escapeRegExp(value: string): string {
|
|
|
554
554
|
|
|
555
555
|
const incompleteRequiredCheckPattern =
|
|
556
556
|
/\b(?:fail(?:ed|s|ing)?|not complete|not completed|incomplete|pending|blocked|planned|todo|to do|to run|will run|not run|not started|not yet|queued)\b/i;
|
|
557
|
+
const packageScriptPrefixPattern = String.raw`(?:pnpm\s+|npm\s+run\s+)?`;
|
|
557
558
|
const requiredCheckNamePattern =
|
|
558
|
-
|
|
559
|
+
new RegExp(
|
|
560
|
+
String.raw`\b(?:${packageScriptPrefixPattern}(?:verify:final|verify:perf|test:browser:perf|test:browser|verify:quick|test|build)|browser performance checkpoint|agent-browser|playwright-fallback)\b`,
|
|
561
|
+
"i",
|
|
562
|
+
);
|
|
559
563
|
const browserPerformanceCheckpointPattern =
|
|
560
564
|
/\b(?:agent-browser|browser performance checkpoint|performance checkpoint|verify:perf|test:browser:perf|playwright-fallback)\b/i;
|
|
561
565
|
const fallbackPerfReasonPattern =
|
|
@@ -599,7 +603,7 @@ function isPassedAgentBrowserPerfRunLine(line: string): boolean {
|
|
|
599
603
|
function isPassedPlaywrightFallbackPerfRunLine(line: string): boolean {
|
|
600
604
|
return (
|
|
601
605
|
isVerificationEvidenceLine(line) &&
|
|
602
|
-
(/\
|
|
606
|
+
(/\b(?:pnpm\s+verify:perf|npm\s+run\s+verify:perf|verify:perf)\b/i.test(line) ||
|
|
603
607
|
(/\bplaywright-fallback\b/i.test(line) &&
|
|
604
608
|
/\b(?:performance|perf|checkpoint)\b/i.test(line))) &&
|
|
605
609
|
!isIncompleteOrSkippedLine(line)
|
|
@@ -643,7 +647,7 @@ function getAgentWorklogVerificationGateErrors(source: string): string[] {
|
|
|
643
647
|
|
|
644
648
|
if (!hasPassedPerfRun && !hasAllowedPerfSkip) {
|
|
645
649
|
errors.push(
|
|
646
|
-
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or
|
|
650
|
+
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or this app's verify:perf script with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
|
|
647
651
|
);
|
|
648
652
|
}
|
|
649
653
|
|
|
@@ -721,7 +725,11 @@ function getAgentWorklogValidationErrors(source: string): string[] {
|
|
|
721
725
|
);
|
|
722
726
|
}
|
|
723
727
|
|
|
724
|
-
if (
|
|
728
|
+
if (
|
|
729
|
+
!/\b(?:pnpm\s+(?:verify|test|build|typecheck)|npm\s+run\s+(?:verify|test|build|typecheck)|browser|Playwright|perf)\b/i.test(
|
|
730
|
+
verificationBody,
|
|
731
|
+
)
|
|
732
|
+
) {
|
|
725
733
|
errors.push("agent-worklog.md Verification must list concrete test/build/browser/performance checks.");
|
|
726
734
|
}
|
|
727
735
|
|
|
@@ -3689,7 +3697,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3689
3697
|
expect.arrayContaining([
|
|
3690
3698
|
"agent-worklog.md required checks must be passed before final delivery; do not report failed, incomplete, pending, or blocked verification.",
|
|
3691
3699
|
"agent-worklog.md must not list required checks as skipped unless they are explicitly not required for a post-first-working non-performance edit.",
|
|
3692
|
-
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or
|
|
3700
|
+
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or this app's verify:perf script with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
|
|
3693
3701
|
]),
|
|
3694
3702
|
);
|
|
3695
3703
|
});
|
|
@@ -3775,7 +3783,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3775
3783
|
`;
|
|
3776
3784
|
|
|
3777
3785
|
expect(getAgentWorklogValidationErrors(missingPerfWorklog)).toContain(
|
|
3778
|
-
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or
|
|
3786
|
+
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or this app's verify:perf script with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
|
|
3779
3787
|
);
|
|
3780
3788
|
});
|
|
3781
3789
|
|
|
@@ -3992,7 +4000,7 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
3992
4000
|
`;
|
|
3993
4001
|
|
|
3994
4002
|
expect(getAgentWorklogValidationErrors(missingFallbackReasonWorklog)).toContain(
|
|
3995
|
-
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or
|
|
4003
|
+
"agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or this app's verify:perf script with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
|
|
3996
4004
|
);
|
|
3997
4005
|
});
|
|
3998
4006
|
|
|
@@ -317,26 +317,54 @@ function sourceUsesDirectStorageApi(): boolean {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
describe("Toolcraft template app performance coverage", () => {
|
|
320
|
-
it("publishes
|
|
320
|
+
it("publishes separate browser acceptance and performance fallback gates", () => {
|
|
321
321
|
const packageJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8")) as {
|
|
322
322
|
scripts?: Record<string, string>;
|
|
323
323
|
};
|
|
324
|
+
const runScriptPattern = (scriptName: string) =>
|
|
325
|
+
`(?:pnpm ${scriptName}|npm run ${scriptName})`;
|
|
324
326
|
|
|
327
|
+
expect(
|
|
328
|
+
packageJson.scripts?.["test:browser"],
|
|
329
|
+
"Generated apps must keep full performance scenarios out of the default browser acceptance gate.",
|
|
330
|
+
).toBe('playwright install chromium && playwright test --grep-invert "browser perf:"');
|
|
325
331
|
expect(
|
|
326
332
|
packageJson.scripts?.["test:browser:perf"],
|
|
327
333
|
"Generated apps must expose a sequential Playwright fallback so perf budgets can be measured when no agent browser is available.",
|
|
328
334
|
).toBe(
|
|
329
|
-
'playwright install chromium && playwright test
|
|
335
|
+
'playwright install chromium && playwright test --grep "browser perf:" --workers=1 --pass-with-no-tests',
|
|
336
|
+
);
|
|
337
|
+
expect(packageJson.scripts?.["verify:quick"]).toMatch(
|
|
338
|
+
new RegExp(`^${runScriptPattern("ai:check")} && ${runScriptPattern("test")}$`),
|
|
339
|
+
);
|
|
340
|
+
expect(packageJson.scripts?.["verify:ui"]).toMatch(
|
|
341
|
+
new RegExp(`^${runScriptPattern("test:browser")}$`),
|
|
342
|
+
);
|
|
343
|
+
expect(packageJson.scripts?.["verify:perf"]).toMatch(
|
|
344
|
+
new RegExp(`^${runScriptPattern("test:browser:perf")}$`),
|
|
345
|
+
);
|
|
346
|
+
expect(packageJson.scripts?.["verify:perf:playwright"]).toMatch(
|
|
347
|
+
new RegExp(`^${runScriptPattern("test:browser:perf")}$`),
|
|
330
348
|
);
|
|
331
|
-
expect(packageJson.scripts?.["verify:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
"pnpm test:browser:perf",
|
|
349
|
+
expect(packageJson.scripts?.["verify:final"]).toMatch(
|
|
350
|
+
new RegExp(
|
|
351
|
+
`^${runScriptPattern("ai:check")} && ${runScriptPattern("test")} && ${runScriptPattern("build")} && ${runScriptPattern("test:browser")}$`,
|
|
352
|
+
),
|
|
336
353
|
);
|
|
337
|
-
|
|
338
|
-
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("keeps the Playwright performance audit inside the browser perf tag", () => {
|
|
357
|
+
const source = readFileSync(join(e2eDir, "app-performance.spec.ts"), "utf8");
|
|
358
|
+
const testNames = [...source.matchAll(/test\(\s*(["'`])([^"'`]+)\1/g)].map(
|
|
359
|
+
(match) => match[2],
|
|
339
360
|
);
|
|
361
|
+
|
|
362
|
+
expect(source).not.toContain("TOOLCRAFT_PERF_CHECK");
|
|
363
|
+
expect(testNames.length).toBeGreaterThan(0);
|
|
364
|
+
expect(
|
|
365
|
+
testNames.every((name) => name.includes("browser perf:")),
|
|
366
|
+
`app-performance.spec.ts tests must all be tagged for the dedicated perf checkpoint: ${testNames.join(", ")}`,
|
|
367
|
+
).toBe(true);
|
|
340
368
|
});
|
|
341
369
|
|
|
342
370
|
it("declares agent browser as the preferred performance runner", () => {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { applySliderValueLabelUnit } from "../slider/slider-value";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const signedNumberPattern = "[-−]?\\d+(?:[.,]\\d+)?";
|
|
4
|
+
const rangePairPattern = new RegExp(
|
|
5
|
+
`(${signedNumberPattern})(?:\\s*[\\p{L}%°]+)?\\s*(?:/|[-‐‑‒–—−])\\s*(${signedNumberPattern})`,
|
|
6
|
+
"u",
|
|
7
|
+
);
|
|
4
8
|
|
|
5
9
|
export function formatRangeSliderValue(
|
|
6
10
|
value: readonly number[],
|
|
@@ -46,11 +50,15 @@ export function parseRangeSliderDraft(
|
|
|
46
50
|
step: number;
|
|
47
51
|
},
|
|
48
52
|
): [number, number] | null {
|
|
49
|
-
const
|
|
50
|
-
const values =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const pairMatch = draftValue.match(rangePairPattern);
|
|
54
|
+
const values = pairMatch
|
|
55
|
+
? [pairMatch[1], pairMatch[2]]
|
|
56
|
+
.map((match) => parseRangeSliderDraftNumber(match))
|
|
57
|
+
.filter(Number.isFinite)
|
|
58
|
+
: Array.from(
|
|
59
|
+
draftValue.matchAll(/[-−]?\d+(?:[.,]\d+)?/g),
|
|
60
|
+
([match]) => parseRangeSliderDraftNumber(match),
|
|
61
|
+
).filter(Number.isFinite);
|
|
54
62
|
|
|
55
63
|
if (values.length === 0) {
|
|
56
64
|
return null;
|
|
@@ -64,3 +72,7 @@ export function parseRangeSliderDraft(
|
|
|
64
72
|
|
|
65
73
|
return sortedValues;
|
|
66
74
|
}
|
|
75
|
+
|
|
76
|
+
function parseRangeSliderDraftNumber(value: string | undefined): number {
|
|
77
|
+
return Number(value?.replace("−", "-").replace(",", "."));
|
|
78
|
+
}
|