reflection-check 0.0.1

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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/dist/adapters/route-manifest.d.ts +3 -0
  4. package/dist/adapters/route-manifest.js +98 -0
  5. package/dist/adapters/route-manifest.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.js +93 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/commands/doctor.d.ts +4 -0
  10. package/dist/commands/doctor.js +5 -0
  11. package/dist/commands/doctor.js.map +1 -0
  12. package/dist/commands/gc.d.ts +8 -0
  13. package/dist/commands/gc.js +45 -0
  14. package/dist/commands/gc.js.map +1 -0
  15. package/dist/commands/review.d.ts +7 -0
  16. package/dist/commands/review.js +149 -0
  17. package/dist/commands/review.js.map +1 -0
  18. package/dist/commands/run.d.ts +10 -0
  19. package/dist/commands/run.js +168 -0
  20. package/dist/commands/run.js.map +1 -0
  21. package/dist/commands/update.d.ts +11 -0
  22. package/dist/commands/update.js +183 -0
  23. package/dist/commands/update.js.map +1 -0
  24. package/dist/contracts/browser/assertions.d.ts +34 -0
  25. package/dist/contracts/browser/assertions.js +87 -0
  26. package/dist/contracts/browser/assertions.js.map +1 -0
  27. package/dist/contracts/browser/browser-contract.d.ts +13 -0
  28. package/dist/contracts/browser/browser-contract.js +35 -0
  29. package/dist/contracts/browser/browser-contract.js.map +1 -0
  30. package/dist/contracts/browser/console-observer.d.ts +6 -0
  31. package/dist/contracts/browser/console-observer.js +14 -0
  32. package/dist/contracts/browser/console-observer.js.map +1 -0
  33. package/dist/contracts/browser/overflow-check.d.ts +6 -0
  34. package/dist/contracts/browser/overflow-check.js +15 -0
  35. package/dist/contracts/browser/overflow-check.js.map +1 -0
  36. package/dist/contracts/browser/route-runner.d.ts +21 -0
  37. package/dist/contracts/browser/route-runner.js +98 -0
  38. package/dist/contracts/browser/route-runner.js.map +1 -0
  39. package/dist/contracts/component/component-visual-contract.d.ts +30 -0
  40. package/dist/contracts/component/component-visual-contract.js +147 -0
  41. package/dist/contracts/component/component-visual-contract.js.map +1 -0
  42. package/dist/contracts/design/command-adapter.d.ts +17 -0
  43. package/dist/contracts/design/command-adapter.js +60 -0
  44. package/dist/contracts/design/command-adapter.js.map +1 -0
  45. package/dist/contracts/design/design-contract.d.ts +8 -0
  46. package/dist/contracts/design/design-contract.js +149 -0
  47. package/dist/contracts/design/design-contract.js.map +1 -0
  48. package/dist/contracts/visual/baseline-compare.d.ts +19 -0
  49. package/dist/contracts/visual/baseline-compare.js +94 -0
  50. package/dist/contracts/visual/baseline-compare.js.map +1 -0
  51. package/dist/contracts/visual/image-diff.d.ts +27 -0
  52. package/dist/contracts/visual/image-diff.js +58 -0
  53. package/dist/contracts/visual/image-diff.js.map +1 -0
  54. package/dist/contracts/visual/thresholds.d.ts +15 -0
  55. package/dist/contracts/visual/thresholds.js +11 -0
  56. package/dist/contracts/visual/thresholds.js.map +1 -0
  57. package/dist/contracts/visual/visual-contract.d.ts +11 -0
  58. package/dist/contracts/visual/visual-contract.js +32 -0
  59. package/dist/contracts/visual/visual-contract.js.map +1 -0
  60. package/dist/core/artifact-store.d.ts +18 -0
  61. package/dist/core/artifact-store.js +105 -0
  62. package/dist/core/artifact-store.js.map +1 -0
  63. package/dist/core/baseline-store.d.ts +18 -0
  64. package/dist/core/baseline-store.js +56 -0
  65. package/dist/core/baseline-store.js.map +1 -0
  66. package/dist/core/config.d.ts +129 -0
  67. package/dist/core/config.js +159 -0
  68. package/dist/core/config.js.map +1 -0
  69. package/dist/core/define-reflection.d.ts +2 -0
  70. package/dist/core/define-reflection.js +4 -0
  71. package/dist/core/define-reflection.js.map +1 -0
  72. package/dist/core/exit-codes.d.ts +7 -0
  73. package/dist/core/exit-codes.js +9 -0
  74. package/dist/core/exit-codes.js.map +1 -0
  75. package/dist/core/failure-classifier.d.ts +3 -0
  76. package/dist/core/failure-classifier.js +19 -0
  77. package/dist/core/failure-classifier.js.map +1 -0
  78. package/dist/core/gc.d.ts +19 -0
  79. package/dist/core/gc.js +161 -0
  80. package/dist/core/gc.js.map +1 -0
  81. package/dist/core/manifest.d.ts +23 -0
  82. package/dist/core/manifest.js +21 -0
  83. package/dist/core/manifest.js.map +1 -0
  84. package/dist/core/redaction.d.ts +3 -0
  85. package/dist/core/redaction.js +63 -0
  86. package/dist/core/redaction.js.map +1 -0
  87. package/dist/core/report-schema.d.ts +262 -0
  88. package/dist/core/report-schema.js +112 -0
  89. package/dist/core/report-schema.js.map +1 -0
  90. package/dist/core/report-writer.d.ts +4 -0
  91. package/dist/core/report-writer.js +77 -0
  92. package/dist/core/report-writer.js.map +1 -0
  93. package/dist/core/server-manager.d.ts +23 -0
  94. package/dist/core/server-manager.js +64 -0
  95. package/dist/core/server-manager.js.map +1 -0
  96. package/dist/core/target-ir.d.ts +64 -0
  97. package/dist/core/target-ir.js +85 -0
  98. package/dist/core/target-ir.js.map +1 -0
  99. package/dist/index.d.ts +2 -0
  100. package/dist/index.js +2 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/integrations/playwright/browser-manager.d.ts +2 -0
  103. package/dist/integrations/playwright/browser-manager.js +5 -0
  104. package/dist/integrations/playwright/browser-manager.js.map +1 -0
  105. package/dist/integrations/playwright/context-factory.d.ts +7 -0
  106. package/dist/integrations/playwright/context-factory.js +19 -0
  107. package/dist/integrations/playwright/context-factory.js.map +1 -0
  108. package/dist/integrations/playwright/trace-policy.d.ts +5 -0
  109. package/dist/integrations/playwright/trace-policy.js +7 -0
  110. package/dist/integrations/playwright/trace-policy.js.map +1 -0
  111. package/dist/integrations/storybook/index-json.d.ts +21 -0
  112. package/dist/integrations/storybook/index-json.js +44 -0
  113. package/dist/integrations/storybook/index-json.js.map +1 -0
  114. package/dist/integrations/storybook/server.d.ts +8 -0
  115. package/dist/integrations/storybook/server.js +23 -0
  116. package/dist/integrations/storybook/server.js.map +1 -0
  117. package/dist/integrations/storybook/story-url.d.ts +2 -0
  118. package/dist/integrations/storybook/story-url.js +13 -0
  119. package/dist/integrations/storybook/story-url.js.map +1 -0
  120. package/dist/utils/process.d.ts +9 -0
  121. package/dist/utils/process.js +69 -0
  122. package/dist/utils/process.js.map +1 -0
  123. package/docs/agent-workflows.md +146 -0
  124. package/docs/artifacts-and-gc.md +125 -0
  125. package/docs/browser-contract.md +98 -0
  126. package/docs/ci.md +44 -0
  127. package/docs/configuration.md +210 -0
  128. package/docs/getting-started.md +166 -0
  129. package/docs/plans/reflection-implementation-plan.md +898 -0
  130. package/docs/target-ir-and-adapters.md +111 -0
  131. package/docs/validation-process.md +172 -0
  132. package/docs/visual-contract.md +174 -0
  133. package/package.json +62 -0
@@ -0,0 +1,94 @@
1
+ import { access, mkdir, readFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { createBaselineStore, createMissingBaselineCheck } from '../../core/baseline-store.js';
4
+ import { comparePngImages } from './image-diff.js';
5
+ export async function compareRouteVisualBaseline(input) {
6
+ const actualArtifact = input.routeCheck.artifacts.find((artifact) => artifact.role === 'actual' && artifact.path.endsWith('.png'));
7
+ const target = `${String(input.routeCheck.metadata.route ?? input.visualCase.route)} ${input.visualCase.viewport}`;
8
+ const baselinePath = resolveCaseBaselinePath(input.visualCase);
9
+ const blocking = input.visualCase.blocking === true || input.visualCase.strict === true;
10
+ const strict = input.visualCase.strict === true || input.visualCase.blocking === true;
11
+ if (!actualArtifact) {
12
+ return {
13
+ id: `visual.${input.visualCase.id}`,
14
+ suite: 'visual',
15
+ target,
16
+ status: blocking ? 'fail' : 'warn',
17
+ severity: blocking ? 'blocking' : 'review',
18
+ summary: `Missing actual screenshot for visual baseline case ${input.visualCase.id}.`,
19
+ artifacts: [],
20
+ metadata: {
21
+ classification: 'missing-actual-screenshot',
22
+ routeId: input.visualCase.route,
23
+ viewport: input.visualCase.viewport,
24
+ baselinePath: input.visualCase.baseline
25
+ },
26
+ suggestedNextStep: 'Add a screenshot expectation to the matching browser route before enabling this visual case.'
27
+ };
28
+ }
29
+ if (!(await pathExists(baselinePath))) {
30
+ return createMissingBaselineCheck({
31
+ id: `visual.${input.visualCase.id}`,
32
+ target,
33
+ baselinePath: input.visualCase.baseline,
34
+ blocking
35
+ });
36
+ }
37
+ const artifactBase = `visual/${input.visualCase.id}`;
38
+ const expectedArtifact = await input.store.writeBuffer(`${artifactBase}/expected.png`, await readFile(baselinePath));
39
+ const actualRunPath = input.store.resolveRunPath(actualArtifact.path);
40
+ const actualVisualArtifact = await input.store.writeBuffer(`${artifactBase}/actual.png`, await readFile(actualRunPath));
41
+ const diffRelativePath = `${artifactBase}/diff.png`;
42
+ const diffPath = input.store.resolveRunPath(diffRelativePath);
43
+ await mkdir(dirname(diffPath), { recursive: true });
44
+ const result = await comparePngImages({
45
+ expectedPath: baselinePath,
46
+ actualPath: input.store.resolveRunPath(actualVisualArtifact.path),
47
+ diffPath,
48
+ ...(input.visualCase.threshold ? { threshold: input.visualCase.threshold } : {}),
49
+ strict
50
+ });
51
+ const diffArtifact = result.diffPath ? await input.store.describeArtifact(diffRelativePath, 'visual-diff', 'diff') : undefined;
52
+ const severity = result.status === 'fail' && blocking ? 'blocking' : 'review';
53
+ return {
54
+ id: `visual.${input.visualCase.id}`,
55
+ suite: 'visual',
56
+ target,
57
+ status: result.status,
58
+ severity,
59
+ summary: createVisualSummary(input.visualCase.id, result),
60
+ artifacts: [expectedArtifact, actualVisualArtifact, ...(diffArtifact ? [diffArtifact] : [])],
61
+ metadata: {
62
+ ...result,
63
+ routeId: input.visualCase.route,
64
+ viewport: input.visualCase.viewport,
65
+ baselinePath: input.visualCase.baseline
66
+ },
67
+ ...(result.status === 'pass'
68
+ ? {}
69
+ : { suggestedNextStep: 'Inspect expected, actual, and diff artifacts. If intentional, update only this visual baseline.' })
70
+ };
71
+ }
72
+ function resolveCaseBaselinePath(visualCase) {
73
+ const store = createBaselineStore(visualCase.baselineRoot ? { rootDir: visualCase.baselineRoot } : {});
74
+ return store.resolveBaselinePath(visualCase.baseline);
75
+ }
76
+ async function pathExists(path) {
77
+ try {
78
+ await access(path);
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ function createVisualSummary(id, result) {
86
+ if (result.classification === 'visual-match') {
87
+ return `${id} matches approved visual baseline.`;
88
+ }
89
+ if (result.classification === 'visual-dimension-mismatch') {
90
+ return `${id} screenshot dimensions differ from approved baseline.`;
91
+ }
92
+ return `${id} differs from approved visual baseline by ${(result.diffRatio * 100).toFixed(2)}% (${result.diffPixels} pixels)${result.thresholdReason ? `, exceeding ${result.thresholdReason}` : ''}.`;
93
+ }
94
+ //# sourceMappingURL=baseline-compare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-compare.js","sourceRoot":"","sources":["../../../src/contracts/visual/baseline-compare.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { type VisualThreshold } from './thresholds.js';
2
+ export type ComparePngImagesInput = {
3
+ expectedPath: string;
4
+ actualPath: string;
5
+ diffPath?: string;
6
+ threshold?: VisualThreshold;
7
+ strict?: boolean;
8
+ };
9
+ export type ImageDimensions = {
10
+ width: number;
11
+ height: number;
12
+ };
13
+ export type ImageDiffResult = {
14
+ status: 'pass' | 'warn' | 'fail';
15
+ classification: 'visual-match' | 'visual-diff' | 'visual-dimension-mismatch';
16
+ width?: number;
17
+ height?: number;
18
+ expected: ImageDimensions;
19
+ actual: ImageDimensions;
20
+ diffPixels: number;
21
+ diffRatio: number;
22
+ dimensionMismatch: boolean;
23
+ threshold?: VisualThreshold;
24
+ thresholdReason?: 'maxDiffPixels' | 'maxDiffPixelRatio';
25
+ diffPath?: string;
26
+ };
27
+ export declare function comparePngImages(input: ComparePngImagesInput): Promise<ImageDiffResult>;
@@ -0,0 +1,58 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import pixelmatch from 'pixelmatch';
3
+ import { PNG } from 'pngjs';
4
+ import { evaluateVisualThreshold } from './thresholds.js';
5
+ export async function comparePngImages(input) {
6
+ const [expected, actual] = await Promise.all([readPng(input.expectedPath), readPng(input.actualPath)]);
7
+ const expectedDimensions = { width: expected.width, height: expected.height };
8
+ const actualDimensions = { width: actual.width, height: actual.height };
9
+ if (expected.width !== actual.width || expected.height !== actual.height) {
10
+ return {
11
+ status: 'fail',
12
+ classification: 'visual-dimension-mismatch',
13
+ expected: expectedDimensions,
14
+ actual: actualDimensions,
15
+ diffPixels: 0,
16
+ diffRatio: 1,
17
+ dimensionMismatch: true,
18
+ ...(input.threshold ? { threshold: input.threshold } : {})
19
+ };
20
+ }
21
+ const diff = new PNG({ width: expected.width, height: expected.height });
22
+ const diffPixels = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, { threshold: 0.1 });
23
+ const thresholdInput = input.threshold
24
+ ? {
25
+ diffPixels,
26
+ totalPixels: expected.width * expected.height,
27
+ threshold: input.threshold
28
+ }
29
+ : {
30
+ diffPixels,
31
+ totalPixels: expected.width * expected.height
32
+ };
33
+ const evaluation = evaluateVisualThreshold(thresholdInput);
34
+ if (input.diffPath) {
35
+ await writeFile(input.diffPath, PNG.sync.write(diff));
36
+ }
37
+ const classification = diffPixels === 0 ? 'visual-match' : 'visual-diff';
38
+ const status = evaluation.passed ? 'pass' : input.strict === true ? 'fail' : 'warn';
39
+ return {
40
+ status,
41
+ classification,
42
+ width: expected.width,
43
+ height: expected.height,
44
+ expected: expectedDimensions,
45
+ actual: actualDimensions,
46
+ diffPixels,
47
+ diffRatio: evaluation.diffRatio,
48
+ dimensionMismatch: false,
49
+ ...(input.threshold ? { threshold: input.threshold } : {}),
50
+ ...(evaluation.reason ? { thresholdReason: evaluation.reason } : {}),
51
+ ...(input.diffPath ? { diffPath: input.diffPath } : {})
52
+ };
53
+ }
54
+ async function readPng(path) {
55
+ const content = await readFile(path);
56
+ return PNG.sync.read(content);
57
+ }
58
+ //# sourceMappingURL=image-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-diff.js","sourceRoot":"","sources":["../../../src/contracts/visual/image-diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export type VisualThreshold = {
2
+ maxDiffPixels?: number | undefined;
3
+ maxDiffPixelRatio?: number | undefined;
4
+ };
5
+ export type ThresholdEvaluationInput = {
6
+ diffPixels: number;
7
+ totalPixels: number;
8
+ threshold?: VisualThreshold;
9
+ };
10
+ export type ThresholdEvaluation = {
11
+ passed: boolean;
12
+ diffRatio: number;
13
+ reason?: 'maxDiffPixels' | 'maxDiffPixelRatio';
14
+ };
15
+ export declare function evaluateVisualThreshold(input: ThresholdEvaluationInput): ThresholdEvaluation;
@@ -0,0 +1,11 @@
1
+ export function evaluateVisualThreshold(input) {
2
+ const diffRatio = input.totalPixels === 0 ? 0 : input.diffPixels / input.totalPixels;
3
+ if (input.threshold?.maxDiffPixels !== undefined && input.diffPixels > input.threshold.maxDiffPixels) {
4
+ return { passed: false, diffRatio, reason: 'maxDiffPixels' };
5
+ }
6
+ if (input.threshold?.maxDiffPixelRatio !== undefined && diffRatio > input.threshold.maxDiffPixelRatio) {
7
+ return { passed: false, diffRatio, reason: 'maxDiffPixelRatio' };
8
+ }
9
+ return { passed: true, diffRatio };
10
+ }
11
+ //# sourceMappingURL=thresholds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thresholds.js","sourceRoot":"","sources":["../../../src/contracts/visual/thresholds.ts"],"names":[],"mappings":"AAiBA,MAAM,UAAU,uBAAuB,CAAC,KAA+B;IACrE,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,WAAW,CAAC;IAErF,IAAI,KAAK,CAAC,SAAS,EAAE,aAAa,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,aAAa,EAAE,CAAC;QACrG,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC/D,CAAC;IAED,IAAI,KAAK,CAAC,SAAS,EAAE,iBAAiB,KAAK,SAAS,IAAI,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,iBAAiB,EAAE,CAAC;QACtG,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IACnE,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AACrC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { ArtifactStore } from '../../core/artifact-store.js';
2
+ import type { CheckResult } from '../../core/report-schema.js';
3
+ import { type RouteVisualBaselineCase } from './baseline-compare.js';
4
+ export type VisualContractConfig = {
5
+ smoke?: RouteVisualBaselineCase[];
6
+ };
7
+ export declare function runRouteVisualSmoke(input: {
8
+ visualSmoke?: RouteVisualBaselineCase[] | undefined;
9
+ browserChecks: CheckResult[];
10
+ store: ArtifactStore;
11
+ }): Promise<CheckResult[]>;
@@ -0,0 +1,32 @@
1
+ import { compareRouteVisualBaseline } from './baseline-compare.js';
2
+ export async function runRouteVisualSmoke(input) {
3
+ if (!input.visualSmoke || input.visualSmoke.length === 0) {
4
+ return [];
5
+ }
6
+ const checks = [];
7
+ for (const visualCase of input.visualSmoke) {
8
+ const routeCheck = input.browserChecks.find((check) => check.metadata.routeId === visualCase.route && check.metadata.viewport === visualCase.viewport);
9
+ if (!routeCheck) {
10
+ checks.push({
11
+ id: `visual.${visualCase.id}`,
12
+ suite: 'visual',
13
+ target: `${visualCase.route} ${visualCase.viewport}`,
14
+ status: visualCase.blocking === true || visualCase.strict === true ? 'fail' : 'warn',
15
+ severity: visualCase.blocking === true || visualCase.strict === true ? 'blocking' : 'review',
16
+ summary: `Missing browser route result for visual case ${visualCase.id}.`,
17
+ artifacts: [],
18
+ metadata: {
19
+ classification: 'missing-browser-route-result',
20
+ routeId: visualCase.route,
21
+ viewport: visualCase.viewport,
22
+ baselinePath: visualCase.baseline
23
+ },
24
+ suggestedNextStep: 'Add a matching browser route and viewport before enabling this visual case.'
25
+ });
26
+ continue;
27
+ }
28
+ checks.push(await compareRouteVisualBaseline({ visualCase, routeCheck, store: input.store }));
29
+ }
30
+ return checks;
31
+ }
32
+ //# sourceMappingURL=visual-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visual-contract.js","sourceRoot":"","sources":["../../../src/contracts/visual/visual-contract.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,0BAA0B,EAAgC,MAAM,uBAAuB,CAAC;AAMjG,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,KAIzC;IACC,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzD,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,KAAK,MAAM,UAAU,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;QAC3C,MAAM,UAAU,GAAG,KAAK,CAAC,aAAa,CAAC,IAAI,CACzC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,KAAK,UAAU,CAAC,KAAK,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,KAAK,UAAU,CAAC,QAAQ,CAC1G,CAAC;QAEF,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC;gBACV,EAAE,EAAE,UAAU,UAAU,CAAC,EAAE,EAAE;gBAC7B,KAAK,EAAE,QAAQ;gBACf,MAAM,EAAE,GAAG,UAAU,CAAC,KAAK,IAAI,UAAU,CAAC,QAAQ,EAAE;gBACpD,MAAM,EAAE,UAAU,CAAC,QAAQ,KAAK,IAAI,IAAI,UAAU,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;gBACpF,QAAQ,EAAE,UAAU,CAAC,QAAQ,KAAK,IAAI,IAAI,UAAU,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ;gBAC5F,OAAO,EAAE,gDAAgD,UAAU,CAAC,EAAE,GAAG;gBACzE,SAAS,EAAE,EAAE;gBACb,QAAQ,EAAE;oBACR,cAAc,EAAE,8BAA8B;oBAC9C,OAAO,EAAE,UAAU,CAAC,KAAK;oBACzB,QAAQ,EAAE,UAAU,CAAC,QAAQ;oBAC7B,YAAY,EAAE,UAAU,CAAC,QAAQ;iBAClC;gBACD,iBAAiB,EAAE,6EAA6E;aACjG,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,MAAM,0BAA0B,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAChG,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { ArtifactRef } from './report-schema.js';
2
+ export type ArtifactStore = {
3
+ rootDir: string;
4
+ runId: string;
5
+ runDir: string;
6
+ ensureRunDir(): Promise<void>;
7
+ resolveRunPath(relativePath: string): string;
8
+ writeText(relativePath: string, content: string): Promise<ArtifactRef>;
9
+ writeBuffer(relativePath: string, content: Buffer): Promise<ArtifactRef>;
10
+ writeJson(relativePath: string, value: unknown): Promise<ArtifactRef>;
11
+ describeArtifact(relativePath: string, type: ArtifactRef['type'], role?: ArtifactRef['role']): Promise<ArtifactRef>;
12
+ updateLatestPointer(): Promise<void>;
13
+ };
14
+ export type CreateArtifactStoreOptions = {
15
+ rootDir?: string;
16
+ runId: string;
17
+ };
18
+ export declare function createArtifactStore(options: CreateArtifactStoreOptions): Promise<ArtifactStore>;
@@ -0,0 +1,105 @@
1
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
+ function assertSafeRelativePath(relativePath) {
5
+ if (relativePath.length === 0 || isAbsolute(relativePath) || relativePath.split(/[\\/]/).includes('..')) {
6
+ throw new Error(`Refusing to write artifact outside run directory: ${relativePath}`);
7
+ }
8
+ }
9
+ function ensureInside(parent, child) {
10
+ const relation = relative(parent, child);
11
+ if (relation.startsWith('..') || isAbsolute(relation)) {
12
+ throw new Error(`Refusing to write artifact outside run directory: ${child}`);
13
+ }
14
+ }
15
+ export async function createArtifactStore(options) {
16
+ const rootDir = resolve(options.rootDir ?? '.reflection');
17
+ const runDir = resolve(rootDir, 'runs', options.runId);
18
+ ensureInside(resolve(rootDir, 'runs'), runDir);
19
+ const resolveRunPath = (relativePath) => {
20
+ assertSafeRelativePath(relativePath);
21
+ const resolved = resolve(runDir, relativePath);
22
+ ensureInside(runDir, resolved);
23
+ return resolved;
24
+ };
25
+ const describeArtifact = async (relativePath, type, role) => {
26
+ const path = resolveRunPath(relativePath);
27
+ const [stats, bytes] = await Promise.all([stat(path), readFile(path)]);
28
+ return {
29
+ type,
30
+ ...(role ? { role } : {}),
31
+ path: relativePath,
32
+ bytes: stats.size,
33
+ sha256: createHash('sha256').update(bytes).digest('hex')
34
+ };
35
+ };
36
+ const writeText = async (relativePath, content) => {
37
+ const path = resolveRunPath(relativePath);
38
+ await mkdir(dirname(path), { recursive: true });
39
+ await writeFile(path, content, 'utf8');
40
+ return describeArtifact(relativePath, inferArtifactType(relativePath), inferArtifactRole(relativePath));
41
+ };
42
+ const writeBuffer = async (relativePath, content) => {
43
+ const path = resolveRunPath(relativePath);
44
+ await mkdir(dirname(path), { recursive: true });
45
+ await writeFile(path, content);
46
+ return describeArtifact(relativePath, inferArtifactType(relativePath), inferArtifactRole(relativePath));
47
+ };
48
+ return {
49
+ rootDir,
50
+ runId: options.runId,
51
+ runDir,
52
+ async ensureRunDir() {
53
+ await mkdir(runDir, { recursive: true });
54
+ },
55
+ resolveRunPath,
56
+ writeText,
57
+ writeBuffer,
58
+ async writeJson(relativePath, value) {
59
+ return writeText(relativePath, `${JSON.stringify(value, null, 2)}\n`);
60
+ },
61
+ describeArtifact,
62
+ async updateLatestPointer() {
63
+ const runsDir = resolve(rootDir, 'runs');
64
+ await mkdir(runsDir, { recursive: true });
65
+ await writeFile(join(runsDir, 'latest'), `${options.runId}\n`, 'utf8');
66
+ }
67
+ };
68
+ }
69
+ function inferArtifactType(relativePath) {
70
+ if (relativePath.endsWith('.json')) {
71
+ return 'metadata';
72
+ }
73
+ if (relativePath.endsWith('.md') || relativePath.endsWith('.html')) {
74
+ return 'report';
75
+ }
76
+ if (relativePath.endsWith('.log')) {
77
+ return 'log';
78
+ }
79
+ if (relativePath.endsWith('/diff.png')) {
80
+ return 'visual-diff';
81
+ }
82
+ if (relativePath.endsWith('.png')) {
83
+ return relativePath.startsWith('visual/') ? 'image' : 'screenshot';
84
+ }
85
+ return 'metadata';
86
+ }
87
+ function inferArtifactRole(relativePath) {
88
+ if (relativePath.startsWith('report.')) {
89
+ return 'evidence';
90
+ }
91
+ if (relativePath.endsWith('.log')) {
92
+ return 'debug';
93
+ }
94
+ if (relativePath.endsWith('/expected.png')) {
95
+ return 'expected';
96
+ }
97
+ if (relativePath.endsWith('/actual.png')) {
98
+ return 'actual';
99
+ }
100
+ if (relativePath.endsWith('/diff.png')) {
101
+ return 'diff';
102
+ }
103
+ return undefined;
104
+ }
105
+ //# sourceMappingURL=artifact-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-store.js","sourceRoot":"","sources":["../../src/core/artifact-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { CheckResult } from './report-schema.js';
2
+ export type BaselineStore = {
3
+ rootDir: string;
4
+ resolveBaselinePath(relativePath: string): string;
5
+ };
6
+ export type CreateBaselineStoreOptions = {
7
+ rootDir?: string;
8
+ };
9
+ export type MissingBaselineCheckOptions = {
10
+ id: string;
11
+ target: string;
12
+ baselinePath: string;
13
+ blocking: boolean;
14
+ metadata?: Record<string, unknown> | undefined;
15
+ };
16
+ export declare function createBaselineStore(options?: CreateBaselineStoreOptions): BaselineStore;
17
+ export declare function readBaselineMetadata(store: BaselineStore, relativePath: string): Promise<Record<string, unknown>>;
18
+ export declare function createMissingBaselineCheck(options: MissingBaselineCheckOptions): CheckResult;
@@ -0,0 +1,56 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { isAbsolute, relative, resolve } from 'node:path';
3
+ function assertSafeRelativePath(relativePath) {
4
+ if (relativePath.length === 0 || isAbsolute(relativePath) || relativePath.split(/[\\/]/).includes('..')) {
5
+ throw new Error(`Refusing to resolve baseline outside baseline directory: ${relativePath}`);
6
+ }
7
+ }
8
+ function ensureInside(parent, child) {
9
+ const relation = relative(parent, child);
10
+ if (relation.startsWith('..') || isAbsolute(relation)) {
11
+ throw new Error(`Refusing to resolve baseline outside baseline directory: ${child}`);
12
+ }
13
+ }
14
+ export function createBaselineStore(options = {}) {
15
+ const rootDir = resolve(options.rootDir ?? '.reflection/baselines');
16
+ return {
17
+ rootDir,
18
+ resolveBaselinePath(relativePath) {
19
+ assertSafeRelativePath(relativePath);
20
+ const resolved = resolve(rootDir, relativePath);
21
+ ensureInside(rootDir, resolved);
22
+ return resolved;
23
+ }
24
+ };
25
+ }
26
+ export async function readBaselineMetadata(store, relativePath) {
27
+ const path = store.resolveBaselinePath(relativePath);
28
+ const raw = await readFile(path, 'utf8');
29
+ const parsed = JSON.parse(raw);
30
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
31
+ throw new Error(`Invalid baseline metadata: ${relativePath}`);
32
+ }
33
+ return parsed;
34
+ }
35
+ export function createMissingBaselineCheck(options) {
36
+ const status = options.blocking ? 'fail' : 'warn';
37
+ const severity = options.blocking ? 'blocking' : 'review';
38
+ return {
39
+ id: options.id,
40
+ suite: 'visual',
41
+ target: options.target,
42
+ status,
43
+ severity,
44
+ summary: `Missing approved visual baseline: ${options.baselinePath}.`,
45
+ artifacts: [],
46
+ metadata: {
47
+ ...(options.metadata ?? {}),
48
+ classification: 'missing-baseline',
49
+ baselinePath: options.baselinePath
50
+ },
51
+ suggestedNextStep: options.blocking
52
+ ? 'Add an approved baseline or mark this visual case as review-only until stable.'
53
+ : 'Review the current screenshot and run reflection update for this specific case if the change is intentional.'
54
+ };
55
+ }
56
+ //# sourceMappingURL=baseline-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-store.js","sourceRoot":"","sources":["../../src/core/baseline-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoB1D,SAAS,sBAAsB,CAAC,YAAoB;IAClD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,YAAY,CAAC,IAAI,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACxG,MAAM,IAAI,KAAK,CAAC,4DAA4D,YAAY,EAAE,CAAC,CAAC;IAC9F,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,MAAc,EAAE,KAAa;IACjD,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACzC,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,4DAA4D,KAAK,EAAE,CAAC,CAAC;IACvF,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,UAAsC,EAAE;IAC1E,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,IAAI,uBAAuB,CAAC,CAAC;IAEpE,OAAO;QACL,OAAO;QACP,mBAAmB,CAAC,YAAoB;YACtC,sBAAsB,CAAC,YAAY,CAAC,CAAC;YACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YAChD,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAChC,OAAO,QAAQ,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,KAAoB,EAAE,YAAoB;IACnF,MAAM,IAAI,GAAG,KAAK,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;IAE1C,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,8BAA8B,YAAY,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,OAAO,MAAiC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,OAAoC;IAC7E,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IAClD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC;IAE1D,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,KAAK,EAAE,QAAQ;QACf,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM;QACN,QAAQ;QACR,OAAO,EAAE,qCAAqC,OAAO,CAAC,YAAY,GAAG;QACrE,SAAS,EAAE,EAAE;QACb,QAAQ,EAAE;YACR,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;YAC3B,cAAc,EAAE,kBAAkB;YAClC,YAAY,EAAE,OAAO,CAAC,YAAY;SACnC;QACD,iBAAiB,EAAE,OAAO,CAAC,QAAQ;YACjC,CAAC,CAAC,gFAAgF;YAClF,CAAC,CAAC,8GAA8G;KACnH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ export declare const runModes: readonly ["smoke", "design", "visual", "full"];
3
+ export type RunMode = (typeof runModes)[number];
4
+ declare const ReflectionConfigSchema: z.ZodObject<{
5
+ project: z.ZodString;
6
+ run: z.ZodDefault<z.ZodObject<{
7
+ defaultMode: z.ZodDefault<z.ZodEnum<{
8
+ smoke: "smoke";
9
+ design: "design";
10
+ visual: "visual";
11
+ full: "full";
12
+ }>>;
13
+ ciMode: z.ZodDefault<z.ZodEnum<{
14
+ smoke: "smoke";
15
+ design: "design";
16
+ visual: "visual";
17
+ full: "full";
18
+ }>>;
19
+ }, z.core.$strip>>;
20
+ contracts: z.ZodObject<{
21
+ browser: z.ZodOptional<z.ZodObject<{
22
+ enabled: z.ZodDefault<z.ZodBoolean>;
23
+ blocking: z.ZodDefault<z.ZodBoolean>;
24
+ baseUrl: z.ZodString;
25
+ server: z.ZodOptional<z.ZodObject<{
26
+ command: z.ZodString;
27
+ readyUrl: z.ZodString;
28
+ reuseExisting: z.ZodDefault<z.ZodBoolean>;
29
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
30
+ }, z.core.$strip>>;
31
+ routes: z.ZodDefault<z.ZodArray<z.ZodObject<{
32
+ id: z.ZodString;
33
+ name: z.ZodOptional<z.ZodString>;
34
+ path: z.ZodString;
35
+ viewports: z.ZodDefault<z.ZodArray<z.ZodString>>;
36
+ expects: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
37
+ urlIncludes: z.ZodString;
38
+ }, z.core.$strip>, z.ZodObject<{
39
+ urlEquals: z.ZodString;
40
+ }, z.core.$strip>, z.ZodObject<{
41
+ role: z.ZodString;
42
+ name: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$strip>, z.ZodObject<{
44
+ label: z.ZodString;
45
+ }, z.core.$strip>, z.ZodObject<{
46
+ text: z.ZodString;
47
+ }, z.core.$strip>, z.ZodObject<{
48
+ noText: z.ZodString;
49
+ }, z.core.$strip>, z.ZodObject<{
50
+ selector: z.ZodString;
51
+ }, z.core.$strip>, z.ZodObject<{
52
+ elementVisible: z.ZodString;
53
+ }, z.core.$strip>, z.ZodObject<{
54
+ elementNotVisible: z.ZodString;
55
+ }, z.core.$strip>, z.ZodObject<{
56
+ noHorizontalOverflow: z.ZodLiteral<true>;
57
+ }, z.core.$strip>, z.ZodObject<{
58
+ noConsoleErrors: z.ZodLiteral<true>;
59
+ }, z.core.$strip>, z.ZodObject<{
60
+ screenshot: z.ZodString;
61
+ }, z.core.$strip>]>>>;
62
+ }, z.core.$strip>>>;
63
+ maskSelectors: z.ZodDefault<z.ZodArray<z.ZodString>>;
64
+ visualSmoke: z.ZodDefault<z.ZodArray<z.ZodObject<{
65
+ id: z.ZodString;
66
+ route: z.ZodString;
67
+ viewport: z.ZodString;
68
+ baseline: z.ZodString;
69
+ baselineRoot: z.ZodOptional<z.ZodString>;
70
+ threshold: z.ZodOptional<z.ZodObject<{
71
+ maxDiffPixels: z.ZodOptional<z.ZodNumber>;
72
+ maxDiffPixelRatio: z.ZodOptional<z.ZodNumber>;
73
+ }, z.core.$strip>>;
74
+ blocking: z.ZodOptional<z.ZodBoolean>;
75
+ strict: z.ZodOptional<z.ZodBoolean>;
76
+ }, z.core.$strip>>>;
77
+ }, z.core.$strip>>;
78
+ design: z.ZodOptional<z.ZodObject<{
79
+ enabled: z.ZodDefault<z.ZodBoolean>;
80
+ commands: z.ZodDefault<z.ZodArray<z.ZodObject<{
81
+ id: z.ZodString;
82
+ command: z.ZodString;
83
+ cwd: z.ZodOptional<z.ZodString>;
84
+ blocking: z.ZodOptional<z.ZodBoolean>;
85
+ }, z.core.$strip>>>;
86
+ }, z.core.$strip>>;
87
+ component: z.ZodOptional<z.ZodObject<{
88
+ enabled: z.ZodDefault<z.ZodBoolean>;
89
+ storybook: z.ZodObject<{
90
+ command: z.ZodString;
91
+ readyUrl: z.ZodString;
92
+ reuseExisting: z.ZodDefault<z.ZodBoolean>;
93
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
94
+ }, z.core.$strip>;
95
+ cases: z.ZodDefault<z.ZodArray<z.ZodObject<{
96
+ id: z.ZodString;
97
+ storyId: z.ZodString;
98
+ viewport: z.ZodDefault<z.ZodString>;
99
+ baseline: z.ZodString;
100
+ baselineRoot: z.ZodOptional<z.ZodString>;
101
+ threshold: z.ZodOptional<z.ZodObject<{
102
+ maxDiffPixels: z.ZodOptional<z.ZodNumber>;
103
+ maxDiffPixelRatio: z.ZodOptional<z.ZodNumber>;
104
+ }, z.core.$strip>>;
105
+ blocking: z.ZodOptional<z.ZodBoolean>;
106
+ strict: z.ZodOptional<z.ZodBoolean>;
107
+ stateNote: z.ZodOptional<z.ZodString>;
108
+ browserState: z.ZodOptional<z.ZodObject<{
109
+ kind: z.ZodEnum<{
110
+ hover: "hover";
111
+ focus: "focus";
112
+ }>;
113
+ selector: z.ZodString;
114
+ animationStabilization: z.ZodObject<{
115
+ disableAnimations: z.ZodOptional<z.ZodBoolean>;
116
+ waitMs: z.ZodOptional<z.ZodNumber>;
117
+ }, z.core.$strip>;
118
+ }, z.core.$strip>>;
119
+ }, z.core.$strip>>>;
120
+ }, z.core.$strip>>;
121
+ visual: z.ZodOptional<z.ZodUnknown>;
122
+ }, z.core.$strip>;
123
+ }, z.core.$strip>;
124
+ export type ReflectionConfigInput = z.input<typeof ReflectionConfigSchema>;
125
+ export type ReflectionConfig = z.output<typeof ReflectionConfigSchema>;
126
+ export declare function isRunMode(value: string): value is RunMode;
127
+ export declare function validateReflectionConfig(input: unknown): ReflectionConfig;
128
+ export declare function loadReflectionConfig(configPath: string): Promise<ReflectionConfig>;
129
+ export {};