@neonwatty/limner 0.1.0

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 (82) hide show
  1. package/README.md +161 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +26 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/capture.d.ts +12 -0
  6. package/dist/commands/capture.js +66 -0
  7. package/dist/commands/capture.js.map +1 -0
  8. package/dist/commands/compare-image-app.d.ts +12 -0
  9. package/dist/commands/compare-image-app.js +45 -0
  10. package/dist/commands/compare-image-app.js.map +1 -0
  11. package/dist/commands/compare-image-reference.d.ts +28 -0
  12. package/dist/commands/compare-image-reference.js +82 -0
  13. package/dist/commands/compare-image-reference.js.map +1 -0
  14. package/dist/commands/compare.d.ts +15 -0
  15. package/dist/commands/compare.js +168 -0
  16. package/dist/commands/compare.js.map +1 -0
  17. package/dist/commands/init.d.ts +12 -0
  18. package/dist/commands/init.js +65 -0
  19. package/dist/commands/init.js.map +1 -0
  20. package/dist/commands/preview.d.ts +2 -0
  21. package/dist/commands/preview.js +27 -0
  22. package/dist/commands/preview.js.map +1 -0
  23. package/dist/commands/report.d.ts +2 -0
  24. package/dist/commands/report.js +14 -0
  25. package/dist/commands/report.js.map +1 -0
  26. package/dist/commands/runs.d.ts +2 -0
  27. package/dist/commands/runs.js +63 -0
  28. package/dist/commands/runs.js.map +1 -0
  29. package/dist/core/dom-metrics.d.ts +18 -0
  30. package/dist/core/dom-metrics.js +54 -0
  31. package/dist/core/dom-metrics.js.map +1 -0
  32. package/dist/core/playwright-capture.d.ts +27 -0
  33. package/dist/core/playwright-capture.js +46 -0
  34. package/dist/core/playwright-capture.js.map +1 -0
  35. package/dist/core/playwright-dom.d.ts +16 -0
  36. package/dist/core/playwright-dom.js +64 -0
  37. package/dist/core/playwright-dom.js.map +1 -0
  38. package/dist/core/reference-dom-facts.d.ts +65 -0
  39. package/dist/core/reference-dom-facts.js +71 -0
  40. package/dist/core/reference-dom-facts.js.map +1 -0
  41. package/dist/core/report-writer.d.ts +43 -0
  42. package/dist/core/report-writer.js +181 -0
  43. package/dist/core/report-writer.js.map +1 -0
  44. package/dist/core/run-logger.d.ts +22 -0
  45. package/dist/core/run-logger.js +67 -0
  46. package/dist/core/run-logger.js.map +1 -0
  47. package/dist/core/side-by-side.d.ts +8 -0
  48. package/dist/core/side-by-side.js +33 -0
  49. package/dist/core/side-by-side.js.map +1 -0
  50. package/dist/core/static-server.d.ts +6 -0
  51. package/dist/core/static-server.js +73 -0
  52. package/dist/core/static-server.js.map +1 -0
  53. package/dist/core/visual-spec-agent-pack.d.ts +27 -0
  54. package/dist/core/visual-spec-agent-pack.js +143 -0
  55. package/dist/core/visual-spec-agent-pack.js.map +1 -0
  56. package/dist/core/visual-spec-prompts.d.ts +32 -0
  57. package/dist/core/visual-spec-prompts.js +129 -0
  58. package/dist/core/visual-spec-prompts.js.map +1 -0
  59. package/dist/core/workspace.d.ts +31 -0
  60. package/dist/core/workspace.js +97 -0
  61. package/dist/core/workspace.js.map +1 -0
  62. package/dist/index.d.ts +14 -0
  63. package/dist/index.js +12 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/schemas/contract.d.ts +43 -0
  66. package/dist/schemas/contract.js +19 -0
  67. package/dist/schemas/contract.js.map +1 -0
  68. package/dist/schemas/events.d.ts +34 -0
  69. package/dist/schemas/events.js +29 -0
  70. package/dist/schemas/events.js.map +1 -0
  71. package/dist/schemas/visual-spec.d.ts +410 -0
  72. package/dist/schemas/visual-spec.js +201 -0
  73. package/dist/schemas/visual-spec.js.map +1 -0
  74. package/docs/agent-workflow.md +45 -0
  75. package/package.json +64 -0
  76. package/skills/limner/SKILL.md +51 -0
  77. package/templates/target/AGENT_GUIDE.md +24 -0
  78. package/templates/target/contract/acceptance.md +23 -0
  79. package/templates/target/contract/regions.json +11 -0
  80. package/templates/target/contract/tokens.json +21 -0
  81. package/templates/target/reference/index.html +22 -0
  82. package/templates/target/reference/styles.css +53 -0
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # Limner
2
+
3
+ Limner is an agent-guided visual fidelity workbench. It helps a coding agent turn an input image into a structured HTML reference, then compare that approved reference against a real app implementation.
4
+
5
+ Limner is model-agnostic. It does not call a vision model and it does not produce hard pass/fail scores. It produces artifacts an agent and human can inspect: screenshots, side-by-sides, DOM metrics, reports, contracts, and local JSONL run logs.
6
+
7
+ The optional visual spec workflow is agent-required. Limner can prepare the prompt pack and validate structured agent output, but it does not extract that spec on its own.
8
+
9
+ ## Install Locally
10
+
11
+ ```bash
12
+ npm install
13
+ npm run build
14
+ ```
15
+
16
+ Run from source during development:
17
+
18
+ ```bash
19
+ npm run dev -- --help
20
+ ```
21
+
22
+ After build:
23
+
24
+ ```bash
25
+ node dist/cli.js --help
26
+ ```
27
+
28
+ Run the full local gate before PRs:
29
+
30
+ ```bash
31
+ npm run check
32
+ ```
33
+
34
+ `npm run check` runs ESLint, TypeScript, Vitest, build, Knip, and a tracked-file line-count guard.
35
+
36
+ ## Basic Workflow
37
+
38
+ ```bash
39
+ limn init ./ideal.png --target replay-boundaries
40
+ limn preview --target replay-boundaries
41
+ limn capture reference --target replay-boundaries
42
+ limn compare image-reference --target replay-boundaries
43
+ limn compare image-app --target replay-boundaries --url http://localhost:3152/internal/optimization-lab/wedding-envelope#replay-boundaries --storage-state ./e2e/.auth/user.json
44
+ limn compare reference-app --target replay-boundaries --url http://localhost:3152/internal/optimization-lab/wedding-envelope#replay-boundaries --storage-state ./e2e/.auth/user.json
45
+ limn report --target replay-boundaries
46
+ limn runs list
47
+ ```
48
+
49
+ ## Workspace Shape
50
+
51
+ ```text
52
+ limner-workspace/
53
+ limner.config.ts
54
+ targets/
55
+ replay-boundaries/
56
+ source/ideal.png
57
+ contract/regions.json
58
+ contract/tokens.json
59
+ contract/acceptance.md
60
+ reference/index.html
61
+ reference/styles.css
62
+ captures/
63
+ reports/
64
+ AGENT_GUIDE.md
65
+ .limner/runs/
66
+ ```
67
+
68
+ ## Modes
69
+
70
+ ### Image To Reference
71
+
72
+ Use this while recreating the input image in standalone HTML/CSS.
73
+
74
+ ```bash
75
+ limn compare image-reference --target replay-boundaries
76
+ limn compare image-reference --target replay-boundaries --spec
77
+ limn compare image-reference --target replay-boundaries --spec --spec-instructions ./visual-spec-policy.md
78
+ ```
79
+
80
+ Outputs:
81
+
82
+ - `captures/image-reference/reference.png`
83
+ - `captures/image-reference/side-by-side.png`
84
+ - `reports/image-reference.md`
85
+
86
+ With `--spec`, Limner also writes an agent handoff pack under `captures/image-reference/spec/`:
87
+
88
+ - `agent-prompt.md`
89
+ - `agent-prompt.codex.md`
90
+ - `agent-prompt.claude.md`
91
+ - `reference-dom-facts.json`
92
+ - `agent-response.schema.json`
93
+ - `agent-response.example.json`
94
+ - expected agent output path: `agent-response.json`
95
+
96
+ Limner also creates `contract/visual-spec-instructions.md` if it does not exist. Edit that file to customize the generated Codex and Claude prompts for a target, or pass `--spec-instructions <path>` to use a shared prompt policy file.
97
+
98
+ After an agent writes a valid `agent-response.json`, rerun the same command and Limner will validate it and split out:
99
+
100
+ - `captures/image-reference/ideal-visual-spec.json`
101
+ - `captures/image-reference/reference-visual-spec.json`
102
+ - `captures/image-reference/visual-spec-diff.json`
103
+
104
+ The prompt pack is tuned for two common flows:
105
+
106
+ - `agent-prompt.codex.md`: for Codex reading local files and writing `agent-response.json` directly in the repo.
107
+ - `agent-prompt.claude.md`: for Claude with attached images and strict structured output, then copying the JSON into `agent-response.json`.
108
+
109
+ Agents should inspect the ideal image and reference screenshot separately. The side-by-side image is comparison context, not the source image to parse.
110
+
111
+ ### Image To App
112
+
113
+ Use this for quick grounding checks against a real app before or during reconstruction.
114
+ It compares the source image directly to a captured app viewport.
115
+
116
+ ```bash
117
+ limn compare image-app --target replay-boundaries --url http://localhost:3152/... --storage-state ./e2e/.auth/user.json
118
+ ```
119
+
120
+ Outputs:
121
+
122
+ - `captures/image-app/app.png`
123
+ - `captures/image-app/side-by-side.png`
124
+ - `reports/image-app.md`
125
+
126
+ ### Reference To App
127
+
128
+ Use this after the reference HTML is approved and the real app needs to match it.
129
+
130
+ ```bash
131
+ limn compare reference-app --target replay-boundaries --url http://localhost:3152/... --storage-state ./e2e/.auth/user.json
132
+ ```
133
+
134
+ Outputs:
135
+
136
+ - `captures/reference-app/reference.png`
137
+ - `captures/reference-app/app.png`
138
+ - `captures/reference-app/side-by-side.png`
139
+ - `captures/reference-app/dom-metrics.json`
140
+ - `reports/reference-app.md`
141
+
142
+ Capture commands default to viewport-only screenshots. Add `--full-page` when the full scrollable page is the comparison target.
143
+
144
+ ## Local Logs
145
+
146
+ Limner writes local structured logs only. There is no remote telemetry.
147
+
148
+ ```text
149
+ .limner/runs/<run-id>/
150
+ manifest.json
151
+ events.jsonl
152
+ agent-notes.md
153
+ ```
154
+
155
+ Use:
156
+
157
+ ```bash
158
+ limn runs list
159
+ limn runs show <run-id>
160
+ limn runs summarize
161
+ ```
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { registerCaptureCommand } from './commands/capture.js';
4
+ import { registerCompareCommand } from './commands/compare.js';
5
+ import { registerInitCommand } from './commands/init.js';
6
+ import { registerPreviewCommand } from './commands/preview.js';
7
+ import { registerReportCommand } from './commands/report.js';
8
+ import { registerRunsCommand } from './commands/runs.js';
9
+ const program = new Command();
10
+ program
11
+ .name('limn')
12
+ .description('Agent-guided visual fidelity workbench')
13
+ .version('0.1.0')
14
+ .option('-w, --workspace <path>', 'Limner workspace root', process.cwd());
15
+ registerInitCommand(program);
16
+ registerPreviewCommand(program);
17
+ registerCaptureCommand(program);
18
+ registerCompareCommand(program);
19
+ registerReportCommand(program);
20
+ registerRunsCommand(program);
21
+ program.parseAsync(process.argv).catch((error) => {
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ console.error(`limn: ${message}`);
24
+ process.exitCode = 1;
25
+ });
26
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,MAAM,CAAC;KACZ,WAAW,CAAC,wCAAwC,CAAC;KACrD,OAAO,CAAC,OAAO,CAAC;KAChB,MAAM,CAAC,wBAAwB,EAAE,uBAAuB,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AAE5E,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC7B,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAE7B,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IACxD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,CAAC,KAAK,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;IAClC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Command } from 'commander';
2
+ export declare function captureReference(options: {
3
+ workspaceRoot: string;
4
+ target: string;
5
+ headed?: boolean;
6
+ fullPage?: boolean;
7
+ modeDir?: string;
8
+ }): Promise<{
9
+ screenshotPath: string;
10
+ metricsPath: string;
11
+ }>;
12
+ export declare function registerCaptureCommand(program: Command): void;
@@ -0,0 +1,66 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { capturePage } from '../core/playwright-capture.js';
4
+ import { createRunLogger } from '../core/run-logger.js';
5
+ import { startStaticServer } from '../core/static-server.js';
6
+ import { loadRegionContract } from '../core/dom-metrics.js';
7
+ import { resolveTarget, resolveWorkspace } from '../core/workspace.js';
8
+ export async function captureReference(options) {
9
+ const workspace = resolveWorkspace(options.workspaceRoot);
10
+ const target = resolveTarget(options.workspaceRoot, options.target);
11
+ const logger = await createRunLogger(workspace, { command: 'capture', target: options.target, mode: 'reference' });
12
+ const captureDir = path.join(target.capturesDir, options.modeDir ?? 'reference');
13
+ const screenshotPath = path.join(captureDir, 'reference.png');
14
+ const metricsPath = path.join(captureDir, 'dom-metrics.json');
15
+ try {
16
+ await logger.event({ type: 'command.started' });
17
+ const contract = await loadRegionContract(target.regionsPath);
18
+ await mkdir(captureDir, { recursive: true });
19
+ const server = await startStaticServer(target.referenceDir);
20
+ try {
21
+ const result = await capturePage({
22
+ url: server.url,
23
+ outputPath: screenshotPath,
24
+ contract,
25
+ side: 'reference',
26
+ headed: options.headed,
27
+ fullPage: options.fullPage,
28
+ });
29
+ await writeFile(metricsPath, JSON.stringify({ reference: result.metrics }, null, 2) + '\n');
30
+ await logger.artifact(screenshotPath, 'screenshot');
31
+ await logger.artifact(metricsPath, 'dom-metrics');
32
+ await logger.complete('ok');
33
+ return { screenshotPath, metricsPath };
34
+ }
35
+ finally {
36
+ await server.close();
37
+ }
38
+ }
39
+ catch (error) {
40
+ await logger.event({ type: 'command.failed', status: 'failed', message: error instanceof Error ? error.message : String(error) });
41
+ await logger.complete('failed');
42
+ throw error;
43
+ }
44
+ }
45
+ export function registerCaptureCommand(program) {
46
+ const capture = program.command('capture').description('Capture Limner artifacts');
47
+ capture
48
+ .command('reference')
49
+ .description('Capture the target reference HTML')
50
+ .requiredOption('-t, --target <name>', 'target name')
51
+ .option('--headed', 'show the browser while capturing')
52
+ .option('--full-page', 'capture the full scrollable page')
53
+ .option('--viewport-only', 'capture only the viewport (default)')
54
+ .action(async (options, command) => {
55
+ const globals = command.optsWithGlobals();
56
+ const result = await captureReference({
57
+ workspaceRoot: globals.workspace,
58
+ target: options.target,
59
+ headed: options.headed,
60
+ fullPage: options.fullPage,
61
+ });
62
+ console.log(`Reference screenshot: ${result.screenshotPath}`);
63
+ console.log(`DOM metrics: ${result.metricsPath}`);
64
+ });
65
+ }
66
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/commands/capture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEvE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAMtC;IACC,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IACnH,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,OAAO,IAAI,WAAW,CAAC,CAAC;IACjF,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;IAC9D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;IAE9D,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;QAChD,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAC9D,MAAM,KAAK,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAC5D,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC;gBAC/B,GAAG,EAAE,MAAM,CAAC,GAAG;gBACf,UAAU,EAAE,cAAc;gBAC1B,QAAQ;gBACR,IAAI,EAAE,WAAW;gBACjB,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;aAC3B,CAAC,CAAC;YACH,MAAM,SAAS,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;YAC5F,MAAM,MAAM,CAAC,QAAQ,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;YACpD,MAAM,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;YAClD,MAAM,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC5B,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC;QACzC,CAAC;gBAAS,CAAC;YACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClI,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAChC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAgB;IACrD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,WAAW,CAAC,0BAA0B,CAAC,CAAC;IAEnF,OAAO;SACJ,OAAO,CAAC,WAAW,CAAC;SACpB,WAAW,CAAC,mCAAmC,CAAC;SAChD,cAAc,CAAC,qBAAqB,EAAE,aAAa,CAAC;SACpD,MAAM,CAAC,UAAU,EAAE,kCAAkC,CAAC;SACtD,MAAM,CAAC,aAAa,EAAE,kCAAkC,CAAC;SACzD,MAAM,CAAC,iBAAiB,EAAE,qCAAqC,CAAC;SAChE,MAAM,CAAC,KAAK,EAAE,OAAiE,EAAE,OAAgB,EAAE,EAAE;QACpG,MAAM,OAAO,GAAG,OAAO,CAAC,eAAe,EAA2B,CAAC;QACnE,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC;YACpC,aAAa,EAAE,OAAO,CAAC,SAAS;YAChC,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;SAC3B,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,yBAAyB,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,gBAAgB,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,12 @@
1
+ export declare function compareImageApp(options: {
2
+ workspaceRoot: string;
3
+ target: string;
4
+ url: string;
5
+ headed?: boolean;
6
+ storageState?: string;
7
+ fullPage?: boolean;
8
+ }): Promise<{
9
+ sideBySidePath: string;
10
+ reportPath: string;
11
+ appPath: string;
12
+ }>;
@@ -0,0 +1,45 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { capturePage } from '../core/playwright-capture.js';
4
+ import { writeImageAppReport } from '../core/report-writer.js';
5
+ import { createRunLogger } from '../core/run-logger.js';
6
+ import { createSideBySideImage } from '../core/side-by-side.js';
7
+ import { resolveTarget, resolveWorkspace } from '../core/workspace.js';
8
+ export async function compareImageApp(options) {
9
+ const workspace = resolveWorkspace(options.workspaceRoot);
10
+ const target = resolveTarget(options.workspaceRoot, options.target);
11
+ const logger = await createRunLogger(workspace, { command: 'compare', target: options.target, mode: 'image-app' });
12
+ const captureDir = path.join(target.capturesDir, 'image-app');
13
+ const appPath = path.join(captureDir, 'app.png');
14
+ const sideBySidePath = path.join(captureDir, 'side-by-side.png');
15
+ try {
16
+ await logger.event({ type: 'command.started' });
17
+ await mkdir(captureDir, { recursive: true });
18
+ await capturePage({
19
+ url: options.url,
20
+ outputPath: appPath,
21
+ headed: options.headed,
22
+ storageState: options.storageState,
23
+ fullPage: options.fullPage,
24
+ });
25
+ await createSideBySideImage({ leftPath: target.sourceImagePath, rightPath: appPath, outputPath: sideBySidePath });
26
+ const reportPath = await writeImageAppReport({
27
+ target,
28
+ idealPath: target.sourceImagePath,
29
+ appPath,
30
+ sideBySidePath,
31
+ appUrl: options.url,
32
+ });
33
+ await logger.artifact(appPath, 'screenshot');
34
+ await logger.artifact(sideBySidePath, 'side-by-side');
35
+ await logger.artifact(reportPath, 'report');
36
+ await logger.complete('ok');
37
+ return { sideBySidePath, reportPath, appPath };
38
+ }
39
+ catch (error) {
40
+ await logger.event({ type: 'command.failed', status: 'failed', message: error instanceof Error ? error.message : String(error) });
41
+ await logger.complete('failed');
42
+ throw error;
43
+ }
44
+ }
45
+ //# sourceMappingURL=compare-image-app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare-image-app.js","sourceRoot":"","sources":["../../src/commands/compare-image-app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEvE,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAOrC;IACC,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IACnH,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IACjD,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;IAEjE,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;QAChD,MAAM,KAAK,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7C,MAAM,WAAW,CAAC;YAChB,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,UAAU,EAAE,OAAO;YACnB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,QAAQ,EAAE,OAAO,CAAC,QAAQ;SAC3B,CAAC,CAAC;QACH,MAAM,qBAAqB,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,eAAe,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC,CAAC;QAClH,MAAM,UAAU,GAAG,MAAM,mBAAmB,CAAC;YAC3C,MAAM;YACN,SAAS,EAAE,MAAM,CAAC,eAAe;YACjC,OAAO;YACP,cAAc;YACd,MAAM,EAAE,OAAO,CAAC,GAAG;SACpB,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAC7C,MAAM,MAAM,CAAC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC;QACtD,MAAM,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAC5C,MAAM,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC5B,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClI,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAChC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,28 @@
1
+ export declare function compareImageReference(options: {
2
+ workspaceRoot: string;
3
+ target: string;
4
+ headed?: boolean;
5
+ fullPage?: boolean;
6
+ spec?: boolean;
7
+ specInstructions?: string;
8
+ }): Promise<{
9
+ sideBySidePath: string;
10
+ reportPath: string;
11
+ specPaths?: {
12
+ idealSpecPath: string;
13
+ referenceSpecPath: string;
14
+ diffPath: string;
15
+ };
16
+ agentSpec?: {
17
+ promptPath: string;
18
+ codexPromptPath: string;
19
+ claudePromptPath: string;
20
+ factsPath: string;
21
+ examplePath: string;
22
+ schemaPath: string;
23
+ instructionsPath: string;
24
+ responsePath: string;
25
+ status: 'awaiting-agent' | 'validated' | 'invalid';
26
+ validationErrors?: string[];
27
+ };
28
+ }>;
@@ -0,0 +1,82 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { writeImageReferenceReport } from '../core/report-writer.js';
4
+ import { createRunLogger } from '../core/run-logger.js';
5
+ import { createSideBySideImage } from '../core/side-by-side.js';
6
+ import { startStaticServer } from '../core/static-server.js';
7
+ import { writeImageReferenceSpecPack } from '../core/visual-spec-agent-pack.js';
8
+ import { resolveTarget, resolveWorkspace } from '../core/workspace.js';
9
+ import { capturePage } from '../core/playwright-capture.js';
10
+ export async function compareImageReference(options) {
11
+ const workspace = resolveWorkspace(options.workspaceRoot);
12
+ const target = resolveTarget(options.workspaceRoot, options.target);
13
+ const logger = await createRunLogger(workspace, { command: 'compare', target: options.target, mode: 'image-reference' });
14
+ const captureDir = path.join(target.capturesDir, 'image-reference');
15
+ const referencePath = path.join(captureDir, 'reference.png');
16
+ const sideBySidePath = path.join(captureDir, 'side-by-side.png');
17
+ let specPaths;
18
+ let agentSpec;
19
+ try {
20
+ await logger.event({ type: 'command.started' });
21
+ await mkdir(captureDir, { recursive: true });
22
+ const server = await startStaticServer(target.referenceDir);
23
+ try {
24
+ await capturePage({
25
+ url: server.url,
26
+ outputPath: referencePath,
27
+ headed: options.headed,
28
+ fullPage: options.fullPage,
29
+ });
30
+ await createSideBySideImage({ leftPath: target.sourceImagePath, rightPath: referencePath, outputPath: sideBySidePath });
31
+ if (options.spec) {
32
+ const specPack = await writeImageReferenceSpecPack({
33
+ target,
34
+ captureDir,
35
+ referenceUrl: server.url,
36
+ referencePath,
37
+ sideBySidePath,
38
+ headed: options.headed,
39
+ instructionsPath: options.specInstructions ? path.resolve(options.workspaceRoot, options.specInstructions) : undefined,
40
+ });
41
+ specPaths = specPack.specPaths;
42
+ agentSpec = specPack;
43
+ }
44
+ }
45
+ finally {
46
+ await server.close();
47
+ }
48
+ const reportPath = await writeImageReferenceReport({
49
+ target,
50
+ idealPath: target.sourceImagePath,
51
+ referencePath,
52
+ sideBySidePath,
53
+ specPaths,
54
+ specHighlights: agentSpec?.status === 'validated' ? agentSpec.diffHighlights : undefined,
55
+ agentSpec,
56
+ });
57
+ await logger.artifact(referencePath, 'screenshot');
58
+ await logger.artifact(sideBySidePath, 'side-by-side');
59
+ if (agentSpec) {
60
+ await logger.artifact(agentSpec.promptPath, 'agent-prompt');
61
+ await logger.artifact(agentSpec.codexPromptPath, 'agent-prompt');
62
+ await logger.artifact(agentSpec.claudePromptPath, 'agent-prompt');
63
+ await logger.artifact(agentSpec.factsPath, 'dom-facts');
64
+ await logger.artifact(agentSpec.examplePath, 'spec-example');
65
+ await logger.artifact(agentSpec.schemaPath, 'spec-schema');
66
+ }
67
+ if (specPaths) {
68
+ await logger.artifact(specPaths.idealSpecPath, 'visual-spec');
69
+ await logger.artifact(specPaths.referenceSpecPath, 'visual-spec');
70
+ await logger.artifact(specPaths.diffPath, 'visual-spec-diff');
71
+ }
72
+ await logger.artifact(reportPath, 'report');
73
+ await logger.complete('ok');
74
+ return { sideBySidePath, reportPath, specPaths, agentSpec };
75
+ }
76
+ catch (error) {
77
+ await logger.event({ type: 'command.failed', status: 'failed', message: error instanceof Error ? error.message : String(error) });
78
+ await logger.complete('failed');
79
+ throw error;
80
+ }
81
+ }
82
+ //# sourceMappingURL=compare-image-reference.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare-image-reference.js","sourceRoot":"","sources":["../../src/commands/compare-image-reference.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { Command } from 'commander';
2
+ export { compareImageReference } from './compare-image-reference.js';
3
+ export declare function compareReferenceApp(options: {
4
+ workspaceRoot: string;
5
+ target: string;
6
+ url: string;
7
+ headed?: boolean;
8
+ storageState?: string;
9
+ fullPage?: boolean;
10
+ }): Promise<{
11
+ sideBySidePath: string;
12
+ reportPath: string;
13
+ metricsPath: string;
14
+ }>;
15
+ export declare function registerCompareCommand(program: Command): void;
@@ -0,0 +1,168 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loadRegionContract } from '../core/dom-metrics.js';
4
+ import { capturePage } from '../core/playwright-capture.js';
5
+ import { writeReferenceAppReport } from '../core/report-writer.js';
6
+ import { createRunLogger } from '../core/run-logger.js';
7
+ import { createSideBySideImage } from '../core/side-by-side.js';
8
+ import { startStaticServer } from '../core/static-server.js';
9
+ import { resolveTarget, resolveWorkspace } from '../core/workspace.js';
10
+ import { compareImageApp } from './compare-image-app.js';
11
+ import { compareImageReference } from './compare-image-reference.js';
12
+ export { compareImageReference } from './compare-image-reference.js';
13
+ export async function compareReferenceApp(options) {
14
+ const workspace = resolveWorkspace(options.workspaceRoot);
15
+ const target = resolveTarget(options.workspaceRoot, options.target);
16
+ const logger = await createRunLogger(workspace, { command: 'compare', target: options.target, mode: 'reference-app' });
17
+ const captureDir = path.join(target.capturesDir, 'reference-app');
18
+ const referencePath = path.join(captureDir, 'reference.png');
19
+ const appPath = path.join(captureDir, 'app.png');
20
+ const sideBySidePath = path.join(captureDir, 'side-by-side.png');
21
+ const metricsPath = path.join(captureDir, 'dom-metrics.json');
22
+ try {
23
+ await logger.event({ type: 'command.started' });
24
+ await mkdir(captureDir, { recursive: true });
25
+ const contract = await loadRegionContract(target.regionsPath);
26
+ const server = await startStaticServer(target.referenceDir);
27
+ let referenceResult;
28
+ try {
29
+ referenceResult = await capturePage({
30
+ url: server.url,
31
+ outputPath: referencePath,
32
+ contract,
33
+ side: 'reference',
34
+ headed: options.headed,
35
+ fullPage: options.fullPage,
36
+ });
37
+ }
38
+ finally {
39
+ await server.close();
40
+ }
41
+ const appResult = await capturePage({
42
+ url: options.url,
43
+ outputPath: appPath,
44
+ contract,
45
+ side: 'app',
46
+ headed: options.headed,
47
+ storageState: options.storageState,
48
+ fullPage: options.fullPage,
49
+ });
50
+ await writeFile(metricsPath, JSON.stringify({
51
+ reference: referenceResult.metrics,
52
+ app: appResult.metrics,
53
+ appConsole: appResult.consoleMessages,
54
+ referenceConsole: referenceResult.consoleMessages,
55
+ }, null, 2) + '\n');
56
+ await createSideBySideImage({ leftPath: referencePath, rightPath: appPath, outputPath: sideBySidePath });
57
+ const reportPath = await writeReferenceAppReport({
58
+ target,
59
+ referencePath,
60
+ appPath,
61
+ sideBySidePath,
62
+ referenceMetrics: referenceResult.metrics,
63
+ appMetrics: appResult.metrics,
64
+ appUrl: options.url,
65
+ });
66
+ await logger.artifact(referencePath, 'screenshot');
67
+ await logger.artifact(appPath, 'screenshot');
68
+ await logger.artifact(sideBySidePath, 'side-by-side');
69
+ await logger.artifact(metricsPath, 'dom-metrics');
70
+ await logger.artifact(reportPath, 'report');
71
+ await logger.complete('ok');
72
+ return { sideBySidePath, reportPath, metricsPath };
73
+ }
74
+ catch (error) {
75
+ await logger.event({ type: 'command.failed', status: 'failed', message: error instanceof Error ? error.message : String(error) });
76
+ await logger.complete('failed');
77
+ throw error;
78
+ }
79
+ }
80
+ export function registerCompareCommand(program) {
81
+ const compare = program.command('compare').description('Compare Limner artifacts');
82
+ compare
83
+ .command('image-reference')
84
+ .description('Compare source image to reference HTML screenshot')
85
+ .requiredOption('-t, --target <name>', 'target name')
86
+ .option('--headed', 'show browser while capturing')
87
+ .option('--full-page', 'capture the full scrollable page')
88
+ .option('--spec', 'prepare or validate an agent-authored visual spec pack')
89
+ .option('--spec-instructions <path>', 'custom visual spec instructions markdown file')
90
+ .option('--viewport-only', 'capture only the viewport (default)')
91
+ .action(async (options, command) => {
92
+ const globals = command.optsWithGlobals();
93
+ const result = await compareImageReference({
94
+ workspaceRoot: globals.workspace,
95
+ target: options.target,
96
+ headed: options.headed,
97
+ fullPage: options.fullPage,
98
+ spec: options.spec,
99
+ specInstructions: options.specInstructions,
100
+ });
101
+ console.log(`Side-by-side: ${result.sideBySidePath}`);
102
+ if (result.agentSpec) {
103
+ console.log(`Shared agent prompt: ${result.agentSpec.promptPath}`);
104
+ console.log(`Codex prompt: ${result.agentSpec.codexPromptPath}`);
105
+ console.log(`Claude prompt: ${result.agentSpec.claudePromptPath}`);
106
+ console.log(`Reference DOM facts: ${result.agentSpec.factsPath}`);
107
+ console.log(`Agent response schema: ${result.agentSpec.schemaPath}`);
108
+ console.log(`Custom instructions: ${result.agentSpec.instructionsPath}`);
109
+ console.log(`Agent response target: ${result.agentSpec.responsePath}`);
110
+ }
111
+ if (result.specPaths) {
112
+ console.log(`Ideal visual spec: ${result.specPaths.idealSpecPath}`);
113
+ console.log(`Reference visual spec: ${result.specPaths.referenceSpecPath}`);
114
+ console.log(`Visual spec diff: ${result.specPaths.diffPath}`);
115
+ }
116
+ else if (options.spec) {
117
+ console.log('Visual spec status: awaiting validated agent response');
118
+ }
119
+ console.log(`Report: ${result.reportPath}`);
120
+ });
121
+ compare
122
+ .command('reference-app')
123
+ .description('Compare reference HTML to an app URL')
124
+ .requiredOption('-t, --target <name>', 'target name')
125
+ .requiredOption('-u, --url <url>', 'app URL')
126
+ .option('--headed', 'show browser while capturing')
127
+ .option('--storage-state <path>', 'Playwright storageState JSON for authenticated app captures')
128
+ .option('--full-page', 'capture the full scrollable page')
129
+ .option('--viewport-only', 'capture only the viewport (default)')
130
+ .action(async (options, command) => {
131
+ const globals = command.optsWithGlobals();
132
+ const result = await compareReferenceApp({
133
+ workspaceRoot: globals.workspace,
134
+ target: options.target,
135
+ url: options.url,
136
+ headed: options.headed,
137
+ storageState: options.storageState,
138
+ fullPage: options.fullPage,
139
+ });
140
+ console.log(`Side-by-side: ${result.sideBySidePath}`);
141
+ console.log(`Metrics: ${result.metricsPath}`);
142
+ console.log(`Report: ${result.reportPath}`);
143
+ });
144
+ compare
145
+ .command('image-app')
146
+ .description('Compare source image directly to an app URL screenshot')
147
+ .requiredOption('-t, --target <name>', 'target name')
148
+ .requiredOption('-u, --url <url>', 'app URL')
149
+ .option('--headed', 'show browser while capturing')
150
+ .option('--storage-state <path>', 'Playwright storageState JSON for authenticated app captures')
151
+ .option('--full-page', 'capture the full scrollable page')
152
+ .option('--viewport-only', 'capture only the viewport (default)')
153
+ .action(async (options, command) => {
154
+ const globals = command.optsWithGlobals();
155
+ const result = await compareImageApp({
156
+ workspaceRoot: globals.workspace,
157
+ target: options.target,
158
+ url: options.url,
159
+ headed: options.headed,
160
+ storageState: options.storageState,
161
+ fullPage: options.fullPage,
162
+ });
163
+ console.log(`App screenshot: ${result.appPath}`);
164
+ console.log(`Side-by-side: ${result.sideBySidePath}`);
165
+ console.log(`Report: ${result.reportPath}`);
166
+ });
167
+ }
168
+ //# sourceMappingURL=compare.js.map