@dailephd/my-dev-kit-lab 0.4.6 → 0.4.7

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 (38) hide show
  1. package/CHANGELOG.md +11 -2
  2. package/README.md +7 -1
  3. package/dist/src/browser/index.js +2 -0
  4. package/dist/src/browser/playwrightRuntime.js +74 -0
  5. package/dist/src/browser/types.js +10 -0
  6. package/dist/src/cli/help.js +76 -0
  7. package/dist/src/cli/runLabCli.js +29 -1
  8. package/dist/src/commands/runTutorialRunCommand.js +170 -0
  9. package/dist/src/commands/runTutorialValidateCommand.js +138 -0
  10. package/dist/src/core/processTree.js +52 -0
  11. package/dist/src/core/resolveCommand.js +34 -0
  12. package/dist/src/core/runMeasuredCommand.js +39 -31
  13. package/dist/src/runtime/index.js +1 -0
  14. package/dist/src/runtime/managedProcess.js +380 -0
  15. package/dist/src/screenshot/captureReportScreenshot.js +30 -27
  16. package/dist/src/tutorial/index.js +16 -0
  17. package/dist/src/tutorial/loadTutorialContracts.js +60 -0
  18. package/dist/src/tutorial/markdownTutorialWriter.js +53 -0
  19. package/dist/src/tutorial/runTutorial.js +465 -0
  20. package/dist/src/tutorial/scenarioValidation.js +383 -0
  21. package/dist/src/tutorial/subtitleWriter.js +88 -0
  22. package/dist/src/tutorial/targetContractValidation.js +212 -0
  23. package/dist/src/tutorial/tutorialActions.js +110 -0
  24. package/dist/src/tutorial/tutorialArtifacts.js +176 -0
  25. package/dist/src/tutorial/tutorialAssertions.js +249 -0
  26. package/dist/src/tutorial/tutorialCursor.js +149 -0
  27. package/dist/src/tutorial/tutorialLocators.js +37 -0
  28. package/dist/src/tutorial/tutorialManifest.js +74 -0
  29. package/dist/src/tutorial/tutorialOverlay.js +180 -0
  30. package/dist/src/tutorial/tutorialPaths.js +86 -0
  31. package/dist/src/tutorial/tutorialSession.js +398 -0
  32. package/dist/src/tutorial/types.js +48 -0
  33. package/dist/src/tutorial/validationPrimitives.js +141 -0
  34. package/examples/tutorial-browser/index.html +47 -0
  35. package/examples/tutorial-browser/prepare.mjs +14 -0
  36. package/examples/tutorial-browser/scenario.json +17 -0
  37. package/examples/tutorial-browser/server.mjs +14 -0
  38. package/package.json +8 -4
@@ -0,0 +1,48 @@
1
+ /**
2
+ * v0.4.7 tutorial domain contracts.
3
+ *
4
+ * Everything here is declarative and serializable by design. A scenario can
5
+ * describe what to do, but it can never carry JavaScript, a shell command, or
6
+ * an arbitrary URL -- those escape hatches are deliberately absent from the
7
+ * union types below and are rejected by validation, not merely undocumented.
8
+ */
9
+ export const TUTORIAL_SCHEMA_VERSION = "1.0.0";
10
+ export const TARGET_CONTRACT_SCHEMA_VERSION = "1.0.0";
11
+ /** Result schema version for TutorialRunResultV1. */
12
+ export const TUTORIAL_RUN_RESULT_SCHEMA_VERSION = "1.0.0";
13
+ export const MAX_TUTORIAL_STEPS = 500;
14
+ export const MAX_STEP_PAUSE_MS = 60_000;
15
+ export const MIN_VIEWPORT_WIDTH = 320;
16
+ export const MAX_VIEWPORT_WIDTH = 3840;
17
+ export const MIN_VIEWPORT_HEIGHT = 240;
18
+ export const MAX_VIEWPORT_HEIGHT = 2160;
19
+ export const DEFAULT_TUTORIAL_ACTION_TIMEOUT_MS = 5_000;
20
+ export const DEFAULT_TUTORIAL_ASSERTION_TIMEOUT_MS = 5_000;
21
+ /** Stable, path-safe, lowercase identifier shape shared by every tutorial id. */
22
+ export const TUTORIAL_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
23
+ /** The only placeholder a trusted target contract may use. */
24
+ export const TARGET_ROOT_PLACEHOLDER = "{{targetRoot}}";
25
+ export const TUTORIAL_LOCATOR_KINDS = ["role", "text", "css", "test-id"];
26
+ export const TUTORIAL_ACTION_TYPES = [
27
+ "goto",
28
+ "click",
29
+ "fill",
30
+ "press",
31
+ "hover",
32
+ "drag",
33
+ "wait-for"
34
+ ];
35
+ export const TUTORIAL_GOTO_WAIT_UNTIL = ["load", "domcontentloaded", "networkidle"];
36
+ export const TUTORIAL_WAIT_FOR_STATES = ["visible", "hidden", "attached", "detached"];
37
+ export const TUTORIAL_ASSERTION_TYPES = [
38
+ "element-visible",
39
+ "text-equals",
40
+ "text-contains",
41
+ "url-path-equals",
42
+ "attribute-equals",
43
+ "http-json-equals",
44
+ "json-file-equals",
45
+ "file-exists"
46
+ ];
47
+ export const TUTORIAL_CALLOUT_PLACEMENTS = ["auto", "top", "right", "bottom", "left"];
48
+ export const TUTORIAL_TARGET_PROCESS_CWDS = ["contract-root", "target-root"];
@@ -0,0 +1,141 @@
1
+ import { TUTORIAL_ID_PATTERN } from "./types.js";
2
+ export function createErrorCollector(source) {
3
+ const errors = [];
4
+ return {
5
+ add(location, message) {
6
+ errors.push(`${source} validation failed at ${location}: ${message}`);
7
+ },
8
+ get errors() {
9
+ return errors;
10
+ },
11
+ get length() {
12
+ return errors.length;
13
+ }
14
+ };
15
+ }
16
+ export function isPlainObject(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+ export function isNonEmptyString(value) {
20
+ return typeof value === "string" && value.trim().length > 0;
21
+ }
22
+ export function isStableId(value) {
23
+ return typeof value === "string" && TUTORIAL_ID_PATTERN.test(value);
24
+ }
25
+ /** Finite positive integer. Rejects NaN, Infinity, zero, negatives and floats. */
26
+ export function isPositiveInteger(value) {
27
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
28
+ }
29
+ /** Finite non-negative integer. Rejects NaN, Infinity, negatives and floats. */
30
+ export function isNonNegativeInteger(value) {
31
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
32
+ }
33
+ export function isJsonScalar(value) {
34
+ return (value === null ||
35
+ typeof value === "string" ||
36
+ typeof value === "boolean" ||
37
+ (typeof value === "number" && Number.isFinite(value)));
38
+ }
39
+ export function describeValue(value) {
40
+ if (value === undefined)
41
+ return "undefined";
42
+ if (value === null)
43
+ return "null";
44
+ if (typeof value === "string")
45
+ return JSON.stringify(value);
46
+ if (typeof value === "number" || typeof value === "boolean")
47
+ return String(value);
48
+ if (Array.isArray(value))
49
+ return "an array";
50
+ if (typeof value === "object")
51
+ return "an object";
52
+ return typeof value;
53
+ }
54
+ /**
55
+ * Enforces the exact supported schema version. Prompt 2 supports 1.0.0 only and
56
+ * never reinterprets or auto-upgrades another version, including another minor.
57
+ */
58
+ export function validateSchemaVersion(errors, value, supported) {
59
+ if (value === undefined) {
60
+ errors.add("schemaVersion", `missing required schemaVersion; expected "${supported}".`);
61
+ return;
62
+ }
63
+ if (typeof value !== "string") {
64
+ errors.add("schemaVersion", `expected the string "${supported}", received ${describeValue(value)}.`);
65
+ return;
66
+ }
67
+ if (value !== supported) {
68
+ errors.add("schemaVersion", `unsupported schemaVersion ${JSON.stringify(value)}; this build supports "${supported}" only.`);
69
+ }
70
+ }
71
+ export function validateStableId(errors, location, value) {
72
+ if (!isNonEmptyString(value)) {
73
+ errors.add(location, `expected a non-empty id string, received ${describeValue(value)}.`);
74
+ return false;
75
+ }
76
+ if (!isStableId(value)) {
77
+ errors.add(location, `id ${JSON.stringify(value)} must match ${String(TUTORIAL_ID_PATTERN)} (lowercase letters, digits, dot, underscore, hyphen; must start with a letter or digit).`);
78
+ return false;
79
+ }
80
+ return true;
81
+ }
82
+ export function validateStringRecord(errors, location, value) {
83
+ if (value === undefined) {
84
+ return undefined;
85
+ }
86
+ if (!isPlainObject(value)) {
87
+ errors.add(location, `expected an object of string values, received ${describeValue(value)}.`);
88
+ return undefined;
89
+ }
90
+ let valid = true;
91
+ for (const [key, entry] of Object.entries(value)) {
92
+ if (key.length === 0) {
93
+ errors.add(location, "environment variable names must not be empty.");
94
+ valid = false;
95
+ continue;
96
+ }
97
+ if (typeof entry !== "string") {
98
+ errors.add(`${location}.${key}`, `expected a string value, received ${describeValue(entry)}.`);
99
+ valid = false;
100
+ }
101
+ }
102
+ return valid ? value : undefined;
103
+ }
104
+ export function validateStringArray(errors, location, value) {
105
+ if (value === undefined) {
106
+ return undefined;
107
+ }
108
+ if (!Array.isArray(value)) {
109
+ errors.add(location, `expected an array of strings, received ${describeValue(value)}.`);
110
+ return undefined;
111
+ }
112
+ let valid = true;
113
+ value.forEach((entry, index) => {
114
+ if (typeof entry !== "string") {
115
+ errors.add(`${location}[${index}]`, `expected a string, received ${describeValue(entry)}.`);
116
+ valid = false;
117
+ }
118
+ });
119
+ return valid ? value : undefined;
120
+ }
121
+ /**
122
+ * Optional timeout fields must be finite positive integers when supplied. Zero,
123
+ * negative values, NaN and Infinity are all rejected rather than silently
124
+ * coerced to a default.
125
+ */
126
+ export function validateOptionalTimeout(errors, location, value) {
127
+ if (value === undefined) {
128
+ return;
129
+ }
130
+ if (!isPositiveInteger(value)) {
131
+ errors.add(location, `expected a finite positive integer number of milliseconds, received ${describeValue(value)}.`);
132
+ }
133
+ }
134
+ export function rejectUnknownKeys(errors, location, value, allowed) {
135
+ const allowedSet = new Set(allowed);
136
+ for (const key of Object.keys(value)) {
137
+ if (!allowedSet.has(key)) {
138
+ errors.add(`${location}.${key}`, `unknown field ${JSON.stringify(key)} is not part of this contract.`);
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,47 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Tutorial Fixture</title>
6
+ <style>
7
+ body { font: 16px/1.5 system-ui, sans-serif; margin: 0; padding: 24px; background: #f6f7f9; color: #111; }
8
+ h1 { margin: 0 0 16px; font-size: 28px; }
9
+ .panel { background: #fff; border: 1px solid #d8dbe0; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
10
+ .row { display: flex; gap: 16px; align-items: flex-start; }
11
+ .col { flex: 1; }
12
+ button { font: inherit; padding: 8px 16px; border-radius: 6px; border: 1px solid #5a6270; background: #fff; cursor: pointer; }
13
+ button:hover { background: #eef1f5; }
14
+ input { font: inherit; padding: 8px; border: 1px solid #b8bec8; border-radius: 6px; width: 100%; box-sizing: border-box; }
15
+ .dropzone { min-height: 72px; border: 2px dashed #9aa3af; border-radius: 8px; padding: 12px; }
16
+ .card { background: #dde7ff; border: 1px solid #5f7fd6; border-radius: 6px; padding: 10px; width: 140px; }
17
+ .hidden { display: none; }
18
+ #status-banner { padding: 10px; border-radius: 6px; background: #e8f5e9; border: 1px solid #66bb6a; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <h1 id="page-title">Tutorial Fixture</h1>
23
+ <div class="panel"><p id="intro-text">Welcome to the generic tutorial fixture.</p><button id="primary-action" data-testid="primary-action" data-state="idle">Activate</button> <span id="activation-count" data-testid="activation-count">0</span></div>
24
+ <div class="panel"><label for="name-field">Your name</label><input id="name-field" data-testid="name-field" aria-label="Your name" /><p id="greeting" data-testid="greeting"></p></div>
25
+ <div class="panel"><button id="hover-target" data-testid="hover-target">Hover me</button><p id="hover-readout" data-testid="hover-readout">not hovered</p></div>
26
+ <div class="panel row"><div class="col"><div id="drag-card" data-testid="drag-card" class="card" draggable="true">Draggable card</div></div><div class="col"><div id="drop-zone" data-testid="drop-zone" class="dropzone">Drop target</div></div></div>
27
+ <div class="panel"><div id="status-banner" data-testid="status-banner" class="hidden">Delayed banner is now visible</div></div>
28
+ <script>
29
+ (() => {
30
+ let activations = 0;
31
+ const primary = document.getElementById("primary-action");
32
+ primary.addEventListener("click", () => { activations += 1; document.getElementById("activation-count").textContent = String(activations); primary.setAttribute("data-state", "activated"); });
33
+ const nameField = document.getElementById("name-field");
34
+ const greeting = document.getElementById("greeting");
35
+ nameField.addEventListener("input", () => { greeting.textContent = nameField.value ? `Hello, ${nameField.value}` : ""; });
36
+ nameField.addEventListener("keydown", event => { if (event.key === "Enter") greeting.textContent = `Submitted: ${nameField.value}`; });
37
+ document.getElementById("hover-target").addEventListener("mouseenter", () => { document.getElementById("hover-readout").textContent = "hovered"; });
38
+ const dragCard = document.getElementById("drag-card");
39
+ const dropZone = document.getElementById("drop-zone");
40
+ dragCard.addEventListener("dragstart", event => event.dataTransfer.setData("text/plain", "drag-card"));
41
+ dropZone.addEventListener("dragover", event => event.preventDefault());
42
+ dropZone.addEventListener("drop", event => { event.preventDefault(); dropZone.textContent = "Dropped"; dropZone.setAttribute("data-dropped", "true"); });
43
+ window.setTimeout(() => document.getElementById("status-banner").classList.remove("hidden"), 300);
44
+ })();
45
+ </script>
46
+ </body>
47
+ </html>
@@ -0,0 +1,14 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const sourceRoot = path.dirname(fileURLToPath(import.meta.url));
6
+ const targetRoot = process.argv[2];
7
+ if (!targetRoot) {
8
+ console.error("prepare requires a target root argument");
9
+ process.exit(2);
10
+ }
11
+ fs.mkdirSync(path.join(targetRoot, "app"), { recursive: true });
12
+ fs.mkdirSync(path.join(targetRoot, "out"), { recursive: true });
13
+ for (const file of ["index.html", "server.mjs"]) fs.copyFileSync(path.join(sourceRoot, file), path.join(targetRoot, "app", file));
14
+ fs.writeFileSync(path.join(targetRoot, "out", "report.json"), JSON.stringify({ prepared: true, counts: { widgets: 3 } }, null, 2), "utf8");
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "id": "lab-browser-fixture",
4
+ "title": "Lab browser fixture tutorial",
5
+ "description": "A generic, lab-owned tutorial that exercises browser actions, locators, assertions and visual declarations against a local static fixture app.",
6
+ "targetId": "lab-browser-fixture",
7
+ "browser": { "viewport": { "width": 1024, "height": 768 } },
8
+ "steps": [
9
+ { "id": "open-app", "narration": "Open the fixture application in the browser.", "action": { "type": "goto", "path": "/", "waitUntil": "load" }, "highlight": { "kind": "css", "selector": "#page-title" }, "callout": { "text": "This is the fixture application.", "locator": { "kind": "css", "selector": "#page-title" }, "placement": "bottom" }, "screenshot": { "id": "app-open" }, "assertions": [ { "type": "url-path-equals", "expected": "/" }, { "type": "element-visible", "locator": { "kind": "role", "role": "heading", "name": "Tutorial Fixture" } }, { "type": "text-equals", "locator": { "kind": "css", "selector": "#page-title" }, "expected": "Tutorial Fixture" }, { "type": "file-exists", "path": "out/report.json" }, { "type": "json-file-equals", "path": "out/report.json", "pointer": "/counts/widgets", "expected": 3 }, { "type": "http-json-equals", "path": "/api/status", "pointer": "/ok", "expected": true } ] },
10
+ { "id": "activate", "narration": "Click the primary action button to activate the panel.", "action": { "type": "click", "locator": { "kind": "test-id", "testId": "primary-action" } }, "highlight": { "kind": "test-id", "testId": "primary-action" }, "callout": { "text": "Clicking here activates the panel.", "placement": "auto", "locator": { "kind": "test-id", "testId": "primary-action" } }, "screenshot": { "id": "activated" }, "assertions": [ { "type": "text-equals", "locator": { "kind": "test-id", "testId": "activation-count" }, "expected": "1" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "primary-action" }, "name": "data-state", "expected": "activated" } ] },
11
+ { "id": "fill-name", "narration": "Type a name into the form field.", "action": { "type": "fill", "locator": { "kind": "test-id", "testId": "name-field" }, "value": "Ada" }, "assertions": [ { "type": "text-contains", "locator": { "kind": "test-id", "testId": "greeting" }, "expected": "Hello, Ada" } ] },
12
+ { "id": "submit-name", "narration": "Press Enter to submit the name.", "action": { "type": "press", "locator": { "kind": "test-id", "testId": "name-field" }, "key": "Enter" }, "screenshot": { "id": "submitted" }, "assertions": [ { "type": "text-contains", "locator": { "kind": "test-id", "testId": "greeting" }, "expected": "Submitted: Ada" } ] },
13
+ { "id": "hover-button", "narration": "Hover the secondary button to reveal its state.", "action": { "type": "hover", "locator": { "kind": "test-id", "testId": "hover-target" } }, "assertions": [ { "type": "text-equals", "locator": { "kind": "test-id", "testId": "hover-readout" }, "expected": "hovered" } ] },
14
+ { "id": "drag-card", "narration": "Drag the card onto the drop target.", "action": { "type": "drag", "source": { "kind": "test-id", "testId": "drag-card" }, "target": { "kind": "test-id", "testId": "drop-zone" } }, "highlight": { "kind": "test-id", "testId": "drop-zone" }, "screenshot": { "id": "dropped" }, "assertions": [ { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "drop-zone" }, "name": "data-dropped", "expected": "true" } ] },
15
+ { "id": "wait-banner", "narration": "Wait for the delayed status banner to appear.", "action": { "type": "wait-for", "locator": { "kind": "text", "text": "Delayed banner is now visible" }, "state": "visible", "timeoutMs": 5000 }, "callout": { "text": "The banner appears after a short delay." }, "screenshot": { "id": "banner-visible" }, "assertions": [ { "type": "element-visible", "locator": { "kind": "test-id", "testId": "status-banner" } } ] }
16
+ ]
17
+ }
@@ -0,0 +1,14 @@
1
+ import http from "node:http";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = path.dirname(fileURLToPath(import.meta.url));
7
+ const port = Number(process.env.TUTORIAL_FIXTURE_PORT || 0);
8
+ const server = http.createServer((req, res) => {
9
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
10
+ if (url.pathname === "/api/status") { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: true, widgets: 3 })); return; }
11
+ const filePath = url.pathname === "/" ? path.join(root, "index.html") : path.join(root, url.pathname);
12
+ fs.readFile(filePath, (error, contents) => { if (error) { res.writeHead(404); res.end("not found"); return; } res.writeHead(200, { "content-type": filePath.endsWith(".html") ? "text/html" : "application/octet-stream" }); res.end(contents); });
13
+ });
14
+ server.listen(port, "127.0.0.1", () => console.log("tutorial fixture listening on " + server.address().port));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dailephd/my-dev-kit-lab",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "type": "module",
5
5
  "description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "dist/src/",
14
14
  "benchmarks/",
15
15
  "docs/METRICS.md",
16
- "examples/token-savings-cases.json"
16
+ "examples/token-savings-cases.json",
17
+ "examples/tutorial-browser/"
17
18
  ],
18
19
  "repository": {
19
20
  "type": "git",
@@ -79,17 +80,20 @@
79
80
  "security:codeql": "tsx scripts/security/runCodeql.ts",
80
81
  "security:semgrep": "tsx scripts/security/runSemgrep.ts",
81
82
  "test:security": "vitest run tests/security/",
83
+ "test:tutorial-browser": "vitest run tests/integration/tutorialRealBrowser.spec.ts",
82
84
  "test:fuzz:smoke": "tsx scripts/security/runFuzzSmoke.ts",
83
85
  "security:validate": "tsx scripts/security/validate.ts",
84
86
  "audit": "tsx scripts/audits/runAudit.ts",
85
87
  "report:context-integrity-smoke": "tsx scripts/report-context-integrity-smoke.ts"
86
88
  },
89
+ "dependencies": {
90
+ "playwright": "1.60.0"
91
+ },
87
92
  "devDependencies": {
88
93
  "@types/node": "^24.0.0",
89
- "playwright": "^1.54.1",
90
94
  "tsx": "^4.20.3",
91
95
  "typescript": "^5.8.3",
92
- "vitest": "^3.2.4"
96
+ "vitest": "^4.1.11"
93
97
  },
94
98
  "overrides": {
95
99
  "vite": "7.3.6"